css nesting to css string

Compact view

Benchmark publishedon

Setup

const cssNestingObj = {
  '.button-wrapper': {
    '--primary-color-default': '#123',
    '--info-color-default': '#233',

    display: 'flex',

    '> .button-a': {
      transform: 'sacle(1.5)',
    },

    '.button-a': {
      transform: 'sacle(1.5)',

      '.button-b': {
        backgroundBlendMode: 'darken',

        '+ .button-c': {
          backgroundBlendMode: 'var(--info-color-default)',
        },
      },
    },
    '&:hover': {
      color: '#123',
    },
    '&:hover div': {
      color: '#123',
    },
    '&::before': {
      content: '""',
    },
    '> .button-c': {
      color: '#123',
    },
  },
  '.button-wrapper2': {
    '--primary-color-default': '#123',
    '--info-color-default': '#233',

    display: 'flex',

    '> .button-a': {
      transform: 'sacle(1.5)',
    },

    '.button-a': {
      transform: 'sacle(1.5)',

      '--primary-color': '#442',
      '--info-color': '#334',

      '.button-b': {
        backgroundBlendMode: 'darken',

        '+ .button-b': {
          backgroundBlendMode: '#726',
        },
      },
    },
    '&:hover': {
      color: '#123',
    },
    '&:hover div': {
      color: '#123',
    },
    '&::before': {
      content: '""',
    },
    '> .button-c': {
      color: '#123',
    },
  },
}

Test Runner

Initializing...

Testing in
Test CaseOps/sec
v1

function toCssPropertyName(value: string) {
  return value.startsWith('--')
    ? value
    : value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
}

/**
 * @param selector
 * @param prefix
 * @example
 * addPrefixClassName('.selector', 'prefix') // => '.prefix-selector'
 */
function addPrefixClassName(selector: string, prefix?: string | null): string {
  if (!prefix) {
    return selector
  }

  return selector.replace(/\.([a-zA-Z_][\w-]*)/g, `.${prefix}-$1`)
}

export function CSSNestingToString(rules: CSSNestingRules, options?: StringifyCSSOptions): string {
  const { minify, prefix } = options ?? {}

  const output: string[] = []

  function visit(rules: CSSNestingRules, depth: number) {
    const indent = minify ? '' : '  '.repeat(depth)

    for (const [selector, body] of Object.entries(rules)) {
      const cssSelector = addPrefixClassName(selector, prefix)

      output.push(minify ? `${cssSelector}{` : `${indent}${cssSelector} {`)

      if (typeof body === 'string') {
        output.push(minify ? body : `${'  '.repeat(depth + 1)}${body}`)
      } else {
        visitBody(body as CSSNestingRules, depth + 1)
      }

      output.push(minify ? '}' : `${indent}}`)
    }
  }

  function visitBody(body: CSSNestingRules, depth: number) {
    const indent = minify ? '' : '  '.repeat(depth)

    for (const [key, value] of Object.entries(body)) {
      if (typeof value === 'string') {
        const property = toCssPropertyName(key)

        output.push(minify ? `${property}:${value};` : `${indent}${property}: ${value};`)

        continue
      }

      const selector = addPrefixClassName(key, prefix)

      output.push(minify ? `${selector}{` : `${indent}${selector} {`)

      visitBody(value as CSSNestingRules, depth + 1)

      output.push(minify ? '}' : `${indent}}`)
    }
  }

  visit(rules, 0)

  return output.join(minify ? '' : '\n')
}

ready
v2
export type CSSValue = string | number | undefined

export type CSSNode = {
  [key: string]: CSSValue | CSSNode
}

export interface StringifyCSSOptions {
  minify?: boolean
  prefix?: string
}

const uppercasePattern = /[A-Z]/g
const classPattern = /\.([a-zA-Z_][\w-]*)/g

function toCssPropertyName(name: string): string {
  if (name.startsWith('--') || !uppercasePattern.test(name)) {
    uppercasePattern.lastIndex = 0
    return name
  }

  uppercasePattern.lastIndex = 0

  return name.replace(uppercasePattern, (char) => `-${char.toLowerCase()}`)
}

function prefixSelector(selector: string, prefix: string): string {
  classPattern.lastIndex = 0

  return selector.replace(
    classPattern,
    `.${prefix}-$1`,
  )
}

function stringifyMinified(
  rules: CSSNode,
  prefix?: string,
): string {
  let css = ''

  function visit(node: CSSNode): void {
    const keys = Object.keys(node)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]!
      const value = node[key]

      if (value === undefined) {
        continue
      }

      if (typeof value === 'object') {
        css += prefix ? prefixSelector(key, prefix) : key
        css += '{'

        visit(value)

        css += '}'
      } else {
        css += toCssPropertyName(key)
        css += ':'
        css += value
        css += ';'
      }
    }
  }

  visit(rules)

  return css
}

function stringifyPretty(
  rules: CSSNode,
  prefix?: string,
): string {
  let css = ''

  function visit(node: CSSNode, depth: number): void {
    const indent = '  '.repeat(depth)
    const keys = Object.keys(node)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]!
      const value = node[key]

      if (value === undefined) {
        continue
      }

      if (typeof value === 'object') {
        css += indent
        css += prefix ? prefixSelector(key, prefix) : key
        css += ' {\n'

        visit(value, depth + 1)

        css += indent
        css += '}\n'
      } else {
        css += indent
        css += toCssPropertyName(key)
        css += ': '
        css += value
        css += ';\n'
      }
    }
  }

  visit(rules, 0)

  return css.slice(0, -1)
}

export function CSSNestingToString(
  rules: CSSNode,
  options?: StringifyCSSOptions,
): string {
  if (options?.minify) {
    return stringifyMinified(rules, options.prefix)
  }

  return stringifyPretty(rules, options?.prefix)
}
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

Revision 1
publishedon