Skip to content

Commit

Permalink
feat: add minifier (#37)
Browse files Browse the repository at this point in the history
Usage:

```js
import { format, minify } from '@projectwallace/format-css'

let minified = minify('a {}')

// which is an alias for

let formatted_mini = format('a {}', { minify: true })
```
  • Loading branch information
bartveneman authored Feb 17, 2024
1 parent 413ba3f commit a768c7d
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 7 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ Need more examples?
1. Multiline tokens like **Selectors, Values, etc.** are rendered on a single line
1. Unknown syntax is rendered as-is, with multi-line formatting kept intact

## Minify CSS

This package also exposes a minifier function since minifying CSS follows many of the same rules as formatting.

```js
import { format, minify } from "@projectwallace/format-css";

let minified = minify("a {}");

// which is an alias for

let formatted_mini = format("a {}", { minify: true });
```

## Acknowledgements

- Thanks to [CSSTree](https://github.com/csstree/csstree) for providing the necessary parser and the interfaces for our CSS Types (the **bold** elements in the list above)
Expand Down
36 changes: 29 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
// @ts-expect-error Typing of css-tree is incomplete
import parse from 'css-tree/parser'

const NEWLINE = '\n'
// Warning: can be overridden when { minify: true }
let NEWLINE = '\n' // or ''
let TAB = '\t' // or ''
let SPACE = ' ' // or ''

/**
* Indent a string
* @param {number} size
* @returns A string with {size} tabs
*/
function indent(size) {
return '\t'.repeat(size)
return TAB.repeat(size)
}

/**
Expand Down Expand Up @@ -196,12 +199,13 @@ function print_selector(node, css, indent_level) {
*/
function print_block(node, css, indent_level) {
let children = node.children
let buffer = SPACE

if (children.isEmpty) {
return ' {}'
return buffer + '{}'
}

let buffer = ' {' + NEWLINE
buffer += '{' + NEWLINE

indent_level++

Expand Down Expand Up @@ -308,7 +312,7 @@ function print_declaration(node, css, indent_level) {
value = value.replace(/\s*\/\s*/, '/')
}

return indent(indent_level) + property + ': ' + value
return indent(indent_level) + property + ':' + SPACE + value
}

/**
Expand Down Expand Up @@ -423,16 +427,34 @@ function print(node, css, indent_level = 0) {
}

/**
* @typedef {Object} Options
* @property {boolean} [minify] Whether to minify the CSS or keep it formatted
*
* Take a string of CSS (minified or not) and format it with some simple rules
* @param {string} css The original CSS
* @returns {string} The newly formatted CSS
* @param {Options} options
* @returns {string} The formatted CSS
*/
export function format(css) {
export function format(css, { minify = false } = {}) {
let ast = parse(css, {
positions: true,
parseAtrulePrelude: false,
parseCustomProperty: true,
parseValue: true,
})

TAB = minify ? '' : '\t'
NEWLINE = minify ? '' : '\n'
SPACE = minify ? '' : ' '

return print(ast, css, 0)
}

/**
* Take a string of CSS and minify it
* @param {string} css The original CSS
* @returns {string} The minified CSS
*/
export function minify(css) {
return format(css, { minify: true })
}
68 changes: 68 additions & 0 deletions test/minify.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { suite } from "uvu"
import * as assert from "uvu/assert"
import { minify } from "../index.js"

let test = suite("Minify")

test('empty rule', () => {
let actual = minify(`a {}`)
let expected = `a{}`
assert.equal(actual, expected)
})

test('simple declaration', () => {
let actual = minify(`:root { --color: red; }`)
let expected = `:root{--color:red;}`
assert.equal(actual, expected)
})

test('simple atrule', () => {
let actual = minify(`@media (min-width: 100px) { body { color: red; } }`)
let expected = `@media (min-width: 100px){body{color:red;}}`
assert.equal(actual, expected)
})

test('empty atrule', () => {
let actual = minify(`@media (min-width: 100px) {}`)
let expected = `@media (min-width: 100px){}`
assert.equal(actual, expected)
})

test("formats multiline values on a single line", () => {
let actual = minify(`
a {
background: linear-gradient(
red,
10% blue,
20% green,100% yellow);
}
`);
let expected = `a{background:linear-gradient(red, 10% blue, 20% green, 100% yellow);}`;
assert.equal(actual, expected);
})

test('Vadim Makeevs example works', () => {
let actual = minify(`
@layer what {
@container (width > 0) {
ul:has(:nth-child(1 of li)) {
@media (height > 0) {
&:hover {
--is: this;
}
}
}
}
}
`)
let expected = `@layer what{@container (width > 0){ul:has(:nth-child(1 of li)){@media (height > 0){&:hover{--is:this;}}}}}`
assert.equal(actual, expected)
})

test('minified Vadims example', () => {
let actual = minify(`@layer what{@container (width>0){@media (min-height:.001px){ul:has(:nth-child(1 of li)):hover{--is:this}}}}`)
let expected = `@layer what{@container (width > 0){@media (min-height: .001px){ul:has(:nth-child(1 of li)):hover{--is:this;}}}}`
assert.equal(actual, expected)
})

test.run()

0 comments on commit a768c7d

Please sign in to comment.