Skip to content

tbela99/css-parser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

playground npm npm cov NPM Downloads bundle size

css-parser

CSS parser and minifier for node and the browser

Installation

From npm

$ npm install @tbela99/css-parser

from jsr

$ deno add @tbela99/css-parser

Features

  • no dependency
  • partial css validation based upon mdn-data: at-rules and selectors validation
  • fault-tolerant parser, will try to fix invalid tokens according to the CSS syntax module 3 recommendations.
  • fast and efficient minification without unsafe transforms, see benchmark
  • minify colors.
  • support css color level 4 & 5: color(), lab(), lch(), oklab(), oklch(), color-mix(), light-dark(), system colors and relative color
  • generate nested css rules
  • convert nested css rules to legacy syntax
  • generate sourcemap
  • compute css shorthands. see supported properties list below
  • evaluate math functions: calc(), clamp(), min(), max(), round(), mod(), rem(), sin(), cos(), tan(), asin(), acos(), atan(), atan2(), pow(), sqrt(), hypot(), log(), exp(), abs(), sign()
  • inline css variables
  • remove duplicate properties
  • flatten @import rules

Playground

Try it online

Exports

There are several ways to import the library into your application.

Node exports

import as a module

import {transform} from '@tbela99/css-parser'; // ...

Deno exports

import as a module

import {transform} from '@tbela99/css-parser'; // ...

import as a CommonJS module

const {transform} = require('@tbela99/css-parser/cjs'); // ...

Web export

Programmatic import

import {transform} from '@tbela99/css-parser/web'; // ...

Javascript module from cdn

<script type="module"> import {transform} from 'https://esm.sh/@tbela99/css-parser@0.9.0/web'; const css = `  .s {   background: color-mix(in hsl, color(display-p3 0 1 0) 80%, yellow); }  `; console.debug(await transform(css).then(r => r.code)); </script>

Javascript module

<script src="dist/web/index.js" type="module"></script>

Single Javascript file

<script src="dist/index-umd-web.js"></script>

Transform

Parse and render css in a single pass.

Usage

transform(css, transformOptions: TransformOptions = {}): TransformResult

Example

import {transform} from '@tbela99/css-parser'; const {ast, code, map, errors, stats} = await transform(css, {minify: true, resolveImport: true, cwd: 'files/css'});

TransformOptions

Include ParseOptions and RenderOptions

ParseOptions

Minify Options

  • minify: boolean, optional. default to true. optimize ast.
  • pass: number, optional. minification pass. default to 1
  • nestingRules: boolean, optional. automatically generated nested rules.
  • expandNestingRules: boolean, optional. convert nesting rules into separate rules. will automatically set nestingRules to false.
  • removeDuplicateDeclarations: boolean, optional. remove duplicate declarations.
  • computeShorthand: boolean, optional. compute shorthand properties.
  • computeCalcExpression: boolean, optional. evaluate calc() expression
  • inlineCssVariables: boolean, optional. replace some css variables with their actual value. they must be declared once in the :root {} or html {} rule.
  • removeEmpty: boolean, optional. remove empty rule lists from the ast.

Validation Options

  • validation: boolean, optional. enable strict css validation using (mdn data)[https://github.com/mdn/data]. only the selector is validated at this time.
  • lenient: boolean, optional. ignore unknown at-rules, pseudo-classes and declarations.

Sourcemap Options

  • src: string, optional. original css file location to be used with sourcemap, also used to resolve url().
  • sourcemap: boolean, optional. preserve node location data.

Ast Traversal Options

  • visitor: VisitorNodeMap, optional. node visitor used to transform the ast.

Urls and @import Options

  • resolveImport: boolean, optional. replace @import rule by the content of the referenced stylesheet.
  • resolveUrls: boolean, optional. resolve css 'url()' according to the parameters 'src' and 'cwd'

Misc Options

  • removeCharset: boolean, optional. remove @charset.
  • cwd: string, optional. destination directory used to resolve url().
  • signal: AbortSignal, optional. abort parsing.

RenderOptions

Minify Options

  • beautify: boolean, optional. default to false. beautify css output.
  • minify: boolean, optional. default to true. minify css values.
  • withParents: boolean, optional. render this node and its parents.
  • removeEmpty: boolean, optional. remove empty rule lists from the ast.
  • expandNestingRules: boolean, optional. expand nesting rules.
  • preserveLicense: boolean, force preserving comments starting with '/*!' when minify is enabled.
  • removeComments: boolean, remove comments in generated css.
  • convertColor: boolean, convert colors to hex.

Sourcemap Options

  • sourcemap: boolean, optional. generate sourcemap

Misc Options

  • indent: string, optional. css indention string. uses space character by default.
  • newLine: string, optional. new line character.
  • output: string, optional. file where to store css. url() are resolved according to the specified value. no file is created though.
  • cwd: string, optional. destination directory used to resolve url().

Parsing

Usage

parse(css, parseOptions = {})

Example

const {ast, errors, stats} = await parse(css);

Rendering

Usage

render(ast, RenderOptions = {});

Examples

Rendering ast

import {parse, render} from '@tbela99/css-parser'; const css = ` @media screen and (min-width: 40em) {  .featurette-heading {  font-size: 50px;  }  .a {  color: red;  width: 3px;  } } `; const result = await parse(css, options); // print declaration without parents console.error(render(result.ast.chi[0].chi[1].chi[1], {withParents: false})); // -> width:3px // print declaration with parents console.debug(render(result.ast.chi[0].chi[1].chi[1], {withParents: true})); // -> @media screen and (min-width:40em){.a{width:3px}}

Merge similar rules

CSS

.clear { width: 0; height: 0; color: transparent; } .clearfix:before { height: 0; width: 0; }
import {transform} from '@tbela99/css-parser'; const result = await transform(css);

Result

.clear, .clearfix:before { height: 0; width: 0 } .clear { color: #0000 }

Automatic CSS Nesting

CSS

const {parse, render} = require("@tbela99/css-parser/cjs"); const css = ` table.colortable td {  text-align:center; } table.colortable td.c {  text-transform:uppercase; } table.colortable td:first-child, table.colortable td:first-child+td {  border:1px solid black; } table.colortable th {  text-align:center;  background:black;  color:white; } `; const result = await parse(css, {nestingRules: true}).then(result => render(result.ast, {minify: false}).code);

Result

table.colortable { & td { text-align: center; &.c { text-transform: uppercase } &:first-child, &:first-child + td { border: 1px solid #000 } } & th { text-align: center; background: #000; color: #fff } }

CSS Validation

CSS

#404 { --animate-duration: 1s; } .s, #404 { --animate-duration: 1s; } .s [type="text" { --animate-duration: 1s; } .s [type="text"]] { --animate-duration: 1s; } .s [type="text"] { --animate-duration: 1s; } .s [type="text" i] { --animate-duration: 1s; } .s [type="text" s] { --animate-duration: 1s ; } .s [type="text" b] { --animate-duration: 1s; } .s [type="text" b],{ --animate-duration: 1s ; } .s [type="text" b] + { --animate-duration: 1s; } .s [type="text" b] + b { --animate-duration: 1s; } .s [type="text" i] + b { --animate-duration: 1s; } .s [type="text"())]{ --animate-duration: 1s; } .s(){ --animate-duration: 1s; } .s:focus { --animate-duration: 1s; }

with validation enabled

import {parse, render} from '@tbela99/css-parser'; const options = {minify: true, validate: true}; const {code} = await parse(css, options).then(result => render(result.ast, {minify: false})); // console.debug(code);
.s:is([type=text],[type=text i],[type=text s],[type=text i]+b,:focus) { --animate-duration: 1s }

with validation disabled

import {parse, render} from '@tbela99/css-parser'; const options = {minify: true, validate: false}; const {code} = await parse(css, options).then(result => render(result.ast, {minify: false})); // console.debug(code);
.s:is([type=text],[type=text i],[type=text s],[type=text b],[type=text b]+b,[type=text i]+b,:focus) { --animate-duration: 1s }

Nested CSS Expansion

CSS

table.colortable { & td { text-align: center; &.c { text-transform: uppercase } &:first-child, &:first-child + td { border: 1px solid #000 } } & th { text-align: center; background: #000; color: #fff } }

Javascript

import {parse, render} from '@tbela99/css-parser'; const options = {minify: true}; const {code} = await parse(css, options).then(result => render(result.ast, {minify: false, expandNestingRules: true})); // console.debug(code);

Result

table.colortable td { text-align: center; } table.colortable td.c { text-transform: uppercase; } table.colortable td:first-child, table.colortable td:first-child + td { border: 1px solid black; } table.colortable th { text-align: center; background: black; color: white; }

Calc() resolution

import {parse, render} from '@tbela99/css-parser'; const css = ` a {  width: calc(100px * log(625, 5)); } .foo-bar {  width: calc(100px * 2);  height: calc(((75.37% - 63.5px) - 900px) + (2 * 100px));  max-width: calc(3.5rem + calc(var(--bs-border-width) * 2)); } `; const prettyPrint = await parse(css).then(result => render(result.ast, {minify: false}).code);

result

a { width: 400px; } .foo-bar { width: 200px; height: calc(75.37% - 763.5px); max-width: calc(3.5rem + var(--bs-border-width) * 2) }

CSS variable inlining

import {parse, render} from '@tbela99/css-parser'; const css = `  :root {  --preferred-width: 20px; } .foo-bar {   width: calc(calc(var(--preferred-width) + 1px) / 3 + 5px);  height: calc(100% / 4);} ` const prettyPrint = await parse(css, {inlineCssVariables: true}).then(result => render(result.ast, {minify: false}).code);

result

.foo-bar { width: 12px; height: 25% }

CSS variable inlining and relative color

import {parse, render} from '@tbela99/css-parser'; const css = `  :root { --color: green; } ._19_u :focus {  color: hsl(from var(--color) calc(h * 2) s l);  } ` const prettyPrint = await parse(css, {inlineCssVariables: true}).then(result => render(result.ast, {minify: false}).code);

result

._19_u :focus { color: navy }

CSS variable inlining and relative color

import {parse, render} from '@tbela99/css-parser'; const css = `  html { --bluegreen: oklab(54.3% -22.5% -5%); } .overlay {  background: oklab(from var(--bluegreen) calc(1.0 - l) calc(a * 0.8) b); } ` const prettyPrint = await parse(css, {inlineCssVariables: true}).then(result => render(result.ast, {minify: false}).code);

result

.overlay { background: #0c6464 }

Node Walker

import {walk} from '@tbela99/css-parser'; for (const {node, parent, root} of walk(ast)) { // do something }

AST

Comment

  • typ: number
  • val: string, the comment

Declaration

  • typ: number
  • nam: string, declaration name
  • val: array of tokens

Rule

  • typ: number
  • sel: string, css selector
  • chi: array of children

AtRule

  • typ: number
  • nam: string. AtRule name
  • val: rule prelude

AtRuleStyleSheet

  • typ: number
  • chi: array of children

KeyFrameRule

  • typ: number
  • sel: string, css selector
  • chi: array of children

Sourcemap

  • sourcemap generation

Minification

  • minify keyframs
  • minify transform
  • evaluate math functions calc(), clamp(), min(), max(), round(), mod(), rem(), sin(), cos(), tan(), asin(), acos(), atan(), atan2(), pow(), sqrt(), hypot(), log(), exp(), abs(), sign()
  • multi-pass minification
  • inline css variables
  • merge identical rules
  • merge adjacent rules
  • minify colors
  • minify numbers and Dimensions tokens
  • compute shorthand: see the list below
  • remove redundant declarations
  • conditionally unwrap :is()
  • automatic css nesting
  • automatically wrap selectors using :is()
  • avoid reparsing (declarations, selectors, at-rule)
  • node and browser versions
  • decode and replace utf-8 escape sequence

Computed shorthands properties

  • all
  • animation
  • background
  • border
  • border-block-end
  • border-block-start
  • border-bottom
  • border-color
  • border-image
  • border-inline-end
  • border-inline-start
  • border-left
  • border-radius
  • border-right
  • border-style
  • border-top
  • border-width
  • column-rule
  • columns
  • container
  • contain-intrinsic-size
  • flex
  • flex-flow
  • font
  • font-synthesis
  • font-variant
  • gap
  • grid
  • grid-area
  • grid-column
  • grid-row
  • grid-template
  • inset
  • list-style
  • margin
  • mask
  • offset
  • outline
  • overflow
  • padding
  • place-content
  • place-items
  • place-self
  • scroll-margin
  • scroll-padding
  • scroll-timeline
  • text-decoration
  • text-emphasis
  • transition

Performance

  • flatten @import

Node Transformation

Ast can be transformed using node visitors

Exemple 1: Declaration

the visitor is called for any declaration encountered

import {AstDeclaration, ParserOptions} from "../src/@types"; const options: ParserOptions = { visitor: { Declaration: (node: AstDeclaration) => { if (node.nam == '-webkit-transform') { node.nam = 'transform' } } } } const css = `  .foo {  -webkit-transform: scale(calc(100 * 2/ 15)); } `; console.debug(await transform(css, options)); // .foo{transform:scale(calc(40/3))}

Exemple 2: Declaration

the visitor is called only on 'height' declarations

import {AstDeclaration, LengthToken, ParserOptions} from "../src/@types"; import {EnumToken} from "../src/lib"; import {transform} from "../src/node"; const options: ParserOptions = { visitor: { Declaration: { // called only for height declaration height: (node: AstDeclaration): AstDeclaration[] => { return [ node, { typ: EnumToken.DeclarationNodeType, nam: 'width', val: [ <LengthToken>{ typ: EnumToken.Length, val: '3', unit: 'px' } ] } ]; } } } }; const css = `  .foo {  height: calc(100px * 2/ 15); } .selector { color: lch(from peru calc(l * 0.8) calc(c * 0.7) calc(h + 180))  } `; console.debug(await transform(css, options)); // .foo{height:calc(40px/3);width:3px}.selector{color:#0880b0}

Exemple 3: At-Rule

the visitor is called on any at-rule

import {AstAtRule, ParserOptions} from "../src/@types"; import {transform} from "../src/node"; const options: ParserOptions = { visitor: { AtRule: (node: AstAtRule): AstAtRule => { if (node.nam == 'media') { return {...node, val: 'all'} } } } }; const css = `  @media screen {    .foo {   height: calc(100px * 2/ 15);   }  } `; console.debug(await transform(css, options)); // .foo{height:calc(40px/3)}

Exemple 4: At-Rule

the visitor is called only for at-rule media

import {AstAtRule, ParserOptions} from "../src/@types"; import {transform} from "../src/node"; const options: ParserOptions = { visitor: { AtRule: { media: (node: AstAtRule): AstAtRule => { return {...node, val: 'all'} } } } }; const css = `  @media screen {    .foo {   height: calc(100px * 2/ 15);   }  } `; console.debug(await transform(css, options)); // .foo{height:calc(40px/3)}

Exemple 5: Rule

the visitor is called on any Rule

import {AstAtRule, ParserOptions} from "../src/@types"; import {transform} from "../src/node"; const options: ParserOptions = { visitor: { Rule(node: AstRule): AstRule { return {...node, sel: '.foo,.bar,.fubar'}; } } }; const css = `   .foo {   height: calc(100px * 2/ 15);   }  `; console.debug(await transform(css, options)); // .foo,.bar,.fubar{height:calc(40px/3)}

Exemple 6: Rule

Adding declarations to any rule

import {transform} from "../src/node"; import {AstRule, ParserOptions} from "../src/@types"; import {parseDeclarations} from "../src/lib"; const options: ParserOptions = { removeEmpty: false, visitor: { Rule: async (node: AstRule): Promise<AstRule | null> => { if (node.sel == '.foo') { node.chi.push(...await parseDeclarations('width: 3px')); return node; } return null; } } }; const css = `  .foo { } `; console.debug(await transform(css, options)); // .foo{width:3px}

Thanks to Jetbrains for sponsoring this project with a free license