css-parser

playground npm npm cov Doc NPM Downloads bundle size

css-parser

CSS parser, minifier and validator for node and the browser

Installation

From npm

$ npm install @tbela99/css-parser

from jsr

$ deno add @tbela99/css-parser

Features

Online documentation

See the full documentation at the CSS Parser documentation site

Playground

Try it online

Import

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

Node

import as a module


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

// ...

Deno

import as a module


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

// ...

import as a CommonJS module


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

// ...

Web

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@1.3.4/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.js" type="module"></script>

Javascript umd module from cdn


<script src="https://unpkg.com/@tbela99/css-parser@1.3.4/dist/index-umd-web.js"></script>
<script>

    (async () => {

        const css = `

.table {
    border-collapse: collapse;
    width: 100%;
}

.table td, .table th {
    border: 1px solid #ddd;
    padding: 8px;
}

.table tr:nth-child(even){background-color: #f2f2f2;}

.table tr:hover {background-color: #ddd;}

.table th {
    padding-top: 12px;
    padding-bottom: 12px;
    text-align: left;
    background-color: #4CAF50;
    color: white;
}
    `;

        console.debug(await CSSParser.transform(css, {beautify: true, convertColor: CSSParser.ColorType.OKLCH}).then(r => r.code));
    })();

</script>

Exported functions


/* parse and render css */
transform(css: string | ReadableStream<string>, options?: TransformOptions = {}): Promise<TransformResult>
/* parse css */
parse(css: string | ReadableStream<string>, options?: ParseOptions = {}): Promise<ParseResult>;
/* render ast */
render(ast: AstNode, options?: RenderOptions = {}): RenderResult;
/* parse and render css file or url */
transformFile(filePathOrUrl: string, options?: TransformOptions = {}, asStream?: boolean): Promise<TransformResult>;
/* parse css file or url */
parseFile(filePathOrUrl: string, options?: ParseOptions = {}, asStream?: boolean): Promise<ParseResult>;

Transform

Parse and render css in a single pass.

Example


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

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

Example

Read from stdin with node using readable stream

import {transform} from "../src/node";
import {Readable} from "node:stream";
import type {TransformResult} from '../src/@types/index.d.ts';

const readableStream: ReadableStream<string> = Readable.toWeb(process.stdin) as ReadableStream<string>;
const result: TransformResult = await transform(readableStream, {beautify: true});

console.log(result.code);

TransformOptions

Include ParseOptions and RenderOptions

ParseOptions

Minify Options

CSS modules Options

CSS Prefix Removal Options

Validation Options

Sourcemap Options

Ast Traversal Options

Urls and \@import Options

Misc Options

RenderOptions

Minify Options

Sourcemap Options

Misc Options

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}}

CSS Modules

CSS modules features are fully supported. refer to the CSS modules documentation for more information.

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

const css = `
.table {
    border-collapse: collapse;
    width: 100%;
}

.table td, .table th {
    border: 1px solid #ddd;
    padding: 8px;
}

.table tr:nth-child(even){background-color: #f2f2f2;}

.table tr:hover {background-color: #ddd;}

.table th {
    padding-top: 12px;
    padding-bottom: 12px;
    text-align: left;
    background-color: #4CAF50;
    color: white;
}
`;

const result = await transform(css, {module: true});

// css code
console.log(result.code);
// css mapping
console.log(result.mapping);

Convert colors

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


const css = `
.hsl { color: #b3222280; }
`;
const result: TransformResult = await transform(css, {
    beautify: true,
    convertColor: ColorType.SRGB
});

console.log(result.css);

result

.hsl {
    color: color(srgb .7019607843137254 .13333333333333333 .13333333333333333/50%)
}

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

Declaration

Rule

AtRule

AtRuleStyleSheet

KeyFrameRule

Sourcemap

Minification

Computed shorthands properties

Performance

Node Transformation

Ast can be transformed using node visitors

Example 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));
}
`;

const result = await transform(css, options);
console.debug(result.code);

// .foo{transform:scale(calc(40/3))}

Example 2: Declaration

the visitor is called only on ‘height’ declarations


import {AstDeclaration, EnumToken, LengthToken, ParserOptions, transform} from '@tbela99/css-parser';

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.LengthTokenType,
                                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)) 
}
`;

const result = await transform(css, options);
console.debug(result.code);

// .foo{height:calc(40px/3);width:3px}.selector{color:#0880b0}

Example 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);    
    } 
}
`;

const result = await transform(css, options);
console.debug(result.code);

// .foo{height:calc(40px/3)}

Example 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 => {

                node.val = 'all';
                return node
            }
        }
    }
};

const css = `

@media screen {
       
    .foo {

            height: calc(100px * 2/ 15);    
    } 
}
`;

const result = await transform(css, options);
console.debug(result.code);

// .foo{height:calc(40px/3)}

Example 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 {

            node.sel = '.foo,.bar,.fubar'
            return node;
        }
    }
};

const css = `

    .foo {

            height: calc(100px * 2/ 15);    
    } 
`;

const result = await transform(css, options);
console.debug(result.code);

// .foo,.bar,.fubar{height:calc(40px/3)}

Example 6: Rule

Adding declarations to any rule


import {AstRule, parseDeclarations, ParserOptions, transform} from '@tbela99/css-parser';

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 {
}
`;

const result = await transform(css, options);
console.debug(result.code);

// .foo{width:3px}