Skip to content

Commit

Permalink
fix minified spaces
Browse files Browse the repository at this point in the history
  • Loading branch information
Bart Veneman committed Feb 18, 2024
1 parent ba32081 commit 15cf035
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 64 deletions.
148 changes: 90 additions & 58 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// @ts-expect-error Typing of css-tree is incomplete
import parse from 'css-tree/parser'

let SPACE = ' '
let EMPTY_STRING = ''

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

/**
* Indent a string
Expand All @@ -20,13 +24,7 @@ function indent(size) {
* @param {string} str
*/
function is_uppercase(str) {
for (let char of str) {
let code = char.charCodeAt(0)
if (code >= 65 && code <= 90) {
return true
}
}
return false
return /[A-Z]/.test(str)
}

/**
Expand All @@ -37,7 +35,7 @@ function is_uppercase(str) {
function substr(node, css) {
let loc = node.loc

if (!loc) return ''
if (!loc) return EMPTY_STRING

let start = loc.start
let end = loc.end
Expand All @@ -49,7 +47,7 @@ function substr(node, css) {
}

// Multi-line nodes, less common
return str.replace(/\s+/g, ' ')
return str.replace(/\s+/g, SPACE)
}

/**
Expand All @@ -59,7 +57,7 @@ function substr(node, css) {
*/
function substr_raw(node, css) {
let loc = node.loc
if (!loc) return ''
if (!loc) return EMPTY_STRING
return css.substring(loc.start.offset, loc.end.offset)
}

Expand All @@ -74,13 +72,14 @@ function print_rule(node, css, indent_level) {
let prelude = node.prelude
let block = node.block

if (prelude !== undefined && prelude.type === 'SelectorList') {
if (prelude.type === 'SelectorList') {
buffer = print_selectorlist(prelude, css, indent_level)
} else {
// In case parsing the selector list fails we'll print it as-is
buffer = print_unknown(prelude, css, indent_level)
}

if (block !== null && block.type === 'Block') {
if (block.type === 'Block') {
buffer += print_block(block, css, indent_level)
}

Expand All @@ -94,10 +93,10 @@ function print_rule(node, css, indent_level) {
* @returns {string} A formatted SelectorList
*/
function print_selectorlist(node, css, indent_level) {
let buffer = ''
let buffer = EMPTY_STRING
let children = node.children

for (let selector of children) {
children.forEach((selector) => {
if (selector.type === 'Selector') {
buffer += print_selector(selector, css, indent_level)
} else {
Expand All @@ -107,7 +106,8 @@ function print_selectorlist(node, css, indent_level) {
if (selector !== children.last) {
buffer += `,` + NEWLINE
}
}
})

return buffer
}

Expand All @@ -116,16 +116,17 @@ function print_selectorlist(node, css, indent_level) {
* @param {string} css
*/
function print_simple_selector(node, css) {
let buffer = ''
let buffer = EMPTY_STRING

if (node.children) {
for (let child of node.children) {
switch (child.type) {
case 'Combinator': {
// putting spaces around `child.name`, unless the combinator is ' '
buffer += ' '
// putting spaces around `child.name` (+ > ~ or ' '), unless the combinator is ' '
buffer += SPACE

if (child.name !== ' ') {
buffer += child.name + ' '
buffer += child.name + SPACE
}
break
}
Expand All @@ -138,15 +139,15 @@ function print_simple_selector(node, css) {
break
}
case 'SelectorList': {
for (let grandchild of child.children) {
child.children.forEach((grandchild, item) => {
if (grandchild.type === 'Selector') {
buffer += print_simple_selector(grandchild, css)
}

if (grandchild !== child.children.last) {
buffer += ', '
if (item.next) {
buffer += ',' + SPACE
}
}
})
break
}
case 'Nth': {
Expand All @@ -160,13 +161,13 @@ function print_simple_selector(node, css) {
}

if (a !== null && b !== null) {
buffer += ' '
buffer += SPACE
}

if (b !== null) {
// When (1n + x) but not (1n - x)
if (a !== null && !b.startsWith('-')) {
buffer += '+ '
buffer += '+' + SPACE
}

buffer += b
Expand All @@ -180,7 +181,7 @@ function print_simple_selector(node, css) {
if (child.selector !== null) {
// `of .selector`
// @ts-expect-error Typing of child.selector is SelectorList, which doesn't seem to be correct
buffer += ' of ' + print_simple_selector(child.selector, css)
buffer += SPACE + 'of' + SPACE + print_simple_selector(child.selector, css)
}
break
}
Expand Down Expand Up @@ -213,7 +214,7 @@ function print_selector(node, css, indent_level) {
*/
function print_block(node, css, indent_level) {
let children = node.children
let buffer = SPACE
let buffer = OPTIONAL_SPACE

if (children.isEmpty) {
return buffer + '{}'
Expand All @@ -223,13 +224,17 @@ function print_block(node, css, indent_level) {

indent_level++

let prev_type

for (let child of children) {
children.forEach((child, item) => {
if (child.type === 'Declaration') {
buffer += print_declaration(child, css, indent_level) + ';'
buffer += print_declaration(child, css, indent_level)

if (child === children.last) {
buffer += LAST_SEMICOLON
} else {
buffer += ';'
}
} else {
if (prev_type === 'Declaration') {
if (item.prev !== null && item.prev.data.type === 'Declaration') {
buffer += NEWLINE
}

Expand All @@ -242,16 +247,14 @@ function print_block(node, css, indent_level) {
}
}

if (child !== children.last) {
if (item.next !== null) {
buffer += NEWLINE

if (child.type !== 'Declaration') {
buffer += NEWLINE
}
}

prev_type = child.type
}
})

indent_level--

Expand All @@ -276,7 +279,7 @@ function print_atrule(node, css, indent_level) {

// @font-face has no prelude
if (prelude !== null) {
buffer += ' ' + print_prelude(prelude, css)
buffer += SPACE + print_prelude(prelude, css)
}

if (block === null) {
Expand Down Expand Up @@ -317,32 +320,31 @@ function print_prelude(node, css) {
function print_declaration(node, css, indent_level) {
let property = node.property

if (!property.startsWith('--') && is_uppercase(property)) {
property = property.toLowerCase()
// Lowercase the property, unless it's a custom property (starts with --)
if (!(property.charCodeAt(0) === 45 && property.charCodeAt(1) === 45)) { // 45 == '-'
if (is_uppercase(property)) {
property = property.toLowerCase()
}
}

let value = print_value(node.value, css).trim()
let value = print_value(node.value, css)

// Special case for `font` shorthand: remove whitespace around /
if (property === 'font') {
value = value.replace(/\s*\/\s*/, '/')
}

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

/**
* @param {import('css-tree').List<import('css-tree').CssNode>} children
* @param {string} css
*/
function print_list(children, css) {
let buffer = ''

for (let node of children) {
if (node !== children.first && node.type !== 'Operator') {
buffer += ' '
}
let buffer = EMPTY_STRING

children.forEach((node, item) => {
if (node.type === 'Identifier') {
buffer += node.name
} else if (node.type === 'Function') {
Expand All @@ -354,16 +356,45 @@ function print_list(children, css) {
// var(--prop, VALUE)
buffer += print_value(node, css)
} else if (node.type === 'Operator') {
// Put extra spacing before + - / *
// but not before a comma
if (node.value !== ',') {
buffer += ' '
// https://developer.mozilla.org/en-US/docs/Web/CSS/calc#notes
// The + and - operators must be surrounded by whitespace
// Whitespace around other operators is optional

// Trim the operator because CSSTree adds whitespace around it
let operator = node.value.trim()
let code = operator.charCodeAt(0)

if (code === 43 || code === 45) { // + or -
// Add required space before + and - operators
buffer += SPACE
} else if (code !== 44) { // ,
// Add optional space before operator
buffer += OPTIONAL_SPACE
}

// FINALLY, render the operator
buffer += operator

if (code === 43 || code === 45) { // + or -
// Add required space after + and - operators
buffer += SPACE
} else {
// Add optional space after other operators (like *, /, and ,)
buffer += OPTIONAL_SPACE
}
buffer += substr(node, css)
} else {
buffer += substr(node, css)
}
}

if (node.type !== 'Operator') {
if (item.next) {
if (item.next.data.type !== 'Operator') {
buffer += SPACE
}
}
}
})

return buffer
}

Expand Down Expand Up @@ -422,7 +453,7 @@ function print_unknown(node, css, indent_level) {
* @returns {string} A formatted Stylesheet
*/
function print(node, css, indent_level = 0) {
let buffer = ''
let buffer = EMPTY_STRING
// @ts-expect-error Property 'children' does not exist on type 'AnPlusB', but we're never using that
let children = node.children

Expand Down Expand Up @@ -460,9 +491,10 @@ export function format(css, { minify = false } = {}) {
parseValue: true,
})

TAB = minify ? '' : '\t'
NEWLINE = minify ? '' : '\n'
SPACE = minify ? '' : ' '
TAB = minify ? EMPTY_STRING : '\t'
NEWLINE = minify ? EMPTY_STRING : '\n'
OPTIONAL_SPACE = minify ? EMPTY_STRING : ' '
LAST_SEMICOLON = minify ? EMPTY_STRING : ';'

return print(ast, css, 0)
}
Expand Down
22 changes: 17 additions & 5 deletions test/minify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ test('empty rule', () => {

test('simple declaration', () => {
let actual = minify(`:root { --color: red; }`)
let expected = `: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;}}`
let expected = `@media (min-width: 100px){body{color:red}}`
assert.equal(actual, expected)
})

Expand All @@ -37,10 +37,22 @@ a {
20% green,100% yellow);
}
`);
let expected = `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('correctly minifies operators', () => {
let actual = minify(`a { width: calc(100% - 10px); }`)
let expected = `a{width:calc(100% - 10px)}`
assert.equal(actual, expected)
})

test('correctly minifiers modern colors', () => {
let actual = minify(`a { color: rgb(0 0 0 / 0.1); }`)
let expected = `a{color:rgb(0 0 0/0.1)}`
assert.equal(actual, expected)
})

test('Vadim Makeevs example works', () => {
let actual = minify(`
@layer what {
Expand All @@ -55,13 +67,13 @@ test('Vadim Makeevs example works', () => {
}
}
`)
let expected = `@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;}}}}`
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)
})

Expand Down
Loading

0 comments on commit 15cf035

Please sign in to comment.