UNPKG

@sequencemedia/css-purge

Version:

A CSS tool written in Node JS as a command line app or library for the purging, burning, reducing, shortening, compressing, cleaning, trimming and formatting of duplicate, extra, excess or bloated CSS.

1,594 lines 51.2 kB
import debug from 'debug'

import path from 'node:path'
import {
  writeFileSync,
  createReadStream,
  existsSync
} from 'node:fs'

import EventEmitter from 'node:events'

import cssTools from '@adobe/css-tools'

import validUrl from 'valid-url'

import jsdom from 'jsdom'

import ROOT from '#where-am-i'
import trim from '#css/trim'
import hack from '#css/hack'

import hasHtml from '#utils/selectors/has-html'

import hasTypeMedia from '#utils/declarations/has-type-media'
import hasTypeDocument from '#utils/declarations/has-type-document'
import hasTypeSupports from '#utils/declarations/has-type-supports'
import hasTypeComment from '#utils/declarations/has-type-comment'

import getTokens from '#utils/get-tokens'
import getSelectors from '#utils/get-selectors'
import getFilePath from '#utils/get-file-path'
import getFileSizeInKB from '#utils/get-file-size-in-kb'
import getSizeInKB from '#utils/get-size-in-kb'
import roundTo from '#utils/round-to'

import {
  handleCssParseError,
  handleOptionsFileReadError,
  handleOptionsFileWriteError,
  handleCssFileReadError,
  handleCssFileWriteError,
  handleHtmlFileReadError
} from '#utils/errors'

import DEFAULT_OPTIONS from '#default-options/default-options' with { type: 'json' }
import DEFAULT_DECLARATION_NAMES from '#default-options/default-declaration-names' with { type: 'json' }

import removeUnused from './remove-unused.mjs'
import processRules from './process-rules.mjs'
import processValues from './process-values.mjs'

const log = debug('@sequencemedia/css-purge')
const info = debug('@sequencemedia/css-purge:info')

const {
  JSDOM
} = jsdom

const DEFAULT_FILE_LOCATION = path.join(ROOT, './default.css')
const DEFAULT_OPTIONS_FILE_LOCATION = path.join(ROOT, './src/default-options/default-options.json')

function toTrim (value) {
  return String(value).trim()
}

function getSummaryStatsFor (collector) {
  return function getSummaryStats ({ declarations, type }) {
    if (Array.isArray(declarations)) {
      collector.noComments = declarations.filter(hasTypeComment).length
    }

    switch (type) {
      case 'rule':
        collector.noRules += 1

        collector.noDeclarations += declarations.length
        break
      case 'comment':
        collector.noComments += 1
        break
      case 'charset':
        collector.noCharset += 1
        break
      case 'custom-media':
        collector.noCustomMedia += 1
        break
      case 'document':
        collector.noDocument += 1
        break
      case 'font-face':
        collector.noFontFace += 1
        break
      case 'host':
        collector.noHost += 1
        break
      case 'import':
        collector.noImport += 1
        break
      case 'keyframes':
        collector.noKeyframes += 1
        break
      case 'keyframe':
        collector.noKeyframe += 1
        break
      case 'media':
        collector.noMedia += 1
        break
      case 'namespace':
        collector.noNamespace += 1
        break
      case 'page':
        collector.noPage += 1
        break
      case 'supports':
        collector.noSupports += 1
        break
    }
  }
}

function toGroups (rules, limit = 4095) {
  const {
    groups
  } = rules.reduce(({ groups, count }, rule) => {
    const group = groups[count] ?? (groups[count] = [])

    group.push(rule)

    if (group.length === limit) count += 1

    return { groups, count }
  }, { groups: [], count: 0 })

  return groups
}

class CSSPurge {
  constructor () {
    const eventEmitter = new EventEmitter()

    const INITIAL_OPTIONS = {
      ...DEFAULT_OPTIONS
    }

    const OPTIONS = {
      ...INITIAL_OPTIONS
    }

    // let FILE_LOCATION
    let OPTIONS_FILE_LOCATION

    // summary
    const SUMMARY = {
      files: {
        output_css: [],
        input_css: [],
        input_html: []
      },
      options: {
        ...INITIAL_OPTIONS
      },
      stats: {},
      duplicate_rules: [],
      duplicate_declarations: [],
      empty_declarations: [],
      selectors_removed: []
    }

    // stats
    const STATS = {
      files: {
        css: [],
        html: []
      },
      before: {
        totalFileSizeKB: 0,
        noNodes: 0,
        noRules: 0,
        noDeclarations: 0,
        noComments: 0,
        noCharset: 0,
        noCustomMedia: 0,
        noDocument: 0,
        noFontFace: 0,
        noHost: 0,
        noImport: 0,
        noKeyframes: 0,
        noKeyframe: 0,
        noMedia: 0,
        noNamespace: 0,
        noPage: 0,
        noSupports: 0
      },
      after: {
        totalFileSizeKB: 0,
        noNodes: 0,
        noRules: 0,
        noDeclarations: 0,
        noComments: 0,
        noCharset: 0,
        noCustomMedia: 0,
        noDocument: 0,
        noFontFace: 0,
        noHost: 0,
        noImport: 0,
        noKeyframes: 0,
        noKeyframe: 0,
        noMedia: 0,
        noNamespace: 0,
        noPage: 0,
        noSupports: 0
      },
      summary: {
        savingsKB: 0,
        savingsPercentage: 0,
        noEmptyDeclarations: 0,
        noDuplicateRules: 0,
        noDuplicateDeclarations: 0,
        noZerosShortened: 0,
        noNamedColorsShortened: 0,
        noHexColorsShortened: 0,
        noRGBColorsShortened: 0,
        noHSLColorsShortened: 0,
        noFontsShortened: 0,
        noBackgroundsShortened: 0,
        noMarginsShortened: 0,
        noPaddingsShortened: 0,
        noListStylesShortened: 0,
        noOutlinesShortened: 0,
        noBordersShortened: 0,
        noBorderTopsShortened: 0,
        noBorderRightsShortened: 0,
        noBorderBottomsShortened: 0,
        noBorderLeftsShortened: 0,
        noBorderTopRightBottomLeftsShortened: 0,
        noBorderRadiusShortened: 0,
        noLastSemiColonsTrimmed: 0,
        noInlineCommentsTrimmed: 0,
        noReductions: {
          noNodes: 0,
          noRules: 0,
          noDeclarations: 0,
          noComments: 0,
          noCharset: 0,
          noCustomMedia: 0,
          noDocument: 0,
          noFontFace: 0,
          noHost: 0,
          noImport: 0,
          noKeyframes: 0,
          noKeyframe: 0,
          noMedia: 0,
          noNamespace: 0,
          noPage: 0,
          noSupports: 0
        }
      }
    }

    const {
      report_file_location: REPORT_FILE_LOCATION,
      reduce_declarations_file_location: REDUCE_DECLARATIONS_FILE_LOCATION
    } = DEFAULT_OPTIONS

    let DEFAULT_OPTIONS_REPORT_FILE_LOCATION = path.join(ROOT, REPORT_FILE_LOCATION)
    let DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION = path.join(ROOT, REDUCE_DECLARATIONS_FILE_LOCATION)

    let HAS_READ_REDUCE_DECLARATIONS = false

    const PARAMS = {
      selector_properties: new Map(),
      selectors: [],
      declaration_names: [
        ...DEFAULT_DECLARATION_NAMES
      ]
    }

    function readOptions (options = {}) {
      const {
        css_file_location: CSS_FILE_LOCATION,
        report_file_location: REPORT_FILE_LOCATION = DEFAULT_OPTIONS_REPORT_FILE_LOCATION,
        reduce_declarations_file_location: REDUCE_DECLARATIONS_FILE_LOCATION = DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION
      } = options

      if (CSS_FILE_LOCATION) SUMMARY.files.output_css.push(CSS_FILE_LOCATION)

      DEFAULT_OPTIONS_REPORT_FILE_LOCATION = REPORT_FILE_LOCATION
      DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION = REDUCE_DECLARATIONS_FILE_LOCATION

      Object.assign(INITIAL_OPTIONS, Object.assign(OPTIONS, { ...options, report_file_location: REPORT_FILE_LOCATION, reduce_declarations_file_location: REDUCE_DECLARATIONS_FILE_LOCATION }))

      SUMMARY.options = {
        ...OPTIONS
      }

      eventEmitter.emit('DEFAULT_OPTIONS_READ_END', OPTIONS)
    } // end of readOptions

    function readOptionsFileLocation (fileLocation = DEFAULT_OPTIONS_FILE_LOCATION) {
      let defaultOptions = ''

      const readStream = createReadStream(fileLocation, 'utf8')

      readStream
        .on('data', (chunk) => {
          defaultOptions += chunk
        })
        .on('end', () => {
          if (defaultOptions !== '') {
            let options
            try {
              options = JSON.parse(defaultOptions)
            } catch (e) {
              eventEmitter.emit('DEFAULT_OPTIONS_READ_ERROR')
              handleOptionsFileReadError(e, DEFAULT_OPTIONS_FILE_LOCATION)
            }

            const {
              css_file_location: CSS_FILE_LOCATION,
              report_file_location: REPORT_FILE_LOCATION = DEFAULT_OPTIONS_REPORT_FILE_LOCATION,
              reduce_declarations_file_location: REDUCE_DECLARATIONS_FILE_LOCATION = DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION
            } = options

            if (CSS_FILE_LOCATION) SUMMARY.files.output_css.push(CSS_FILE_LOCATION)

            DEFAULT_OPTIONS_REPORT_FILE_LOCATION = REPORT_FILE_LOCATION
            DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION = REDUCE_DECLARATIONS_FILE_LOCATION

            Object.assign(INITIAL_OPTIONS, Object.assign(OPTIONS, { ...options, report_file_location: REPORT_FILE_LOCATION, reduce_declarations_file_location: REDUCE_DECLARATIONS_FILE_LOCATION }))

            SUMMARY.options = {
              ...OPTIONS
            }
          }

          eventEmitter.emit('DEFAULT_OPTIONS_READ_END', OPTIONS)
        })
        .on('error', (e) => {
          eventEmitter.emit('DEFAULT_OPTIONS_READ_ERROR')
          handleOptionsFileReadError(e, DEFAULT_OPTIONS_FILE_LOCATION)
        })

      return readStream
    } // end of readOptionsFileLocation

    function readReduceDeclarations (reduceDeclarations = {}) {
      const {
        declaration_names: declarationNames = [],
        selectors = {}
      } = reduceDeclarations

      const SELECTOR_PROPERTIES = new Map()
      let SELECTORS = []
      let DECLARATION_NAMES = [
        ...DEFAULT_DECLARATION_NAMES
      ]

      switch (typeof selectors) {
        case 'object':
          Object
            .entries(selectors)
            .forEach(([selector, properties]) => {
              SELECTOR_PROPERTIES.set(selector, properties.replace(/^\s+|\s+$/g, '').replace(/\r?\n|\r/g, '').split(',').map(toTrim).filter(Boolean))
            })

          SELECTORS = Array.from(SELECTOR_PROPERTIES.keys())
          break
        case 'string':
          SELECTORS = (
            selectors.length
              ? selectors.replace(/^\s+|\s+$/g, '').replace(/\r?\n|\r/g, '').split(',').map(toTrim).filter(Boolean)
              : []
          )
          break
      }

      // by name
      if (typeof declarationNames === 'string') {
        DECLARATION_NAMES = declarationNames.replace(/^\s+|\s+$/g, '').split(',').map(toTrim).filter(Boolean)
      } else {
        if (Array.isArray(declarationNames)) {
          DECLARATION_NAMES = declarationNames.map(toTrim).filter(Boolean)
        }
      }

      PARAMS.selector_properties = SELECTOR_PROPERTIES
      PARAMS.selectors = SELECTORS
      PARAMS.declaration_names = DECLARATION_NAMES

      HAS_READ_REDUCE_DECLARATIONS = true

      eventEmitter.emit('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_END', OPTIONS)
    } // end of readReduceDeclarations

    function readReduceDeclarationsFileLocation (fileLocation = DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION) {
      let reduceDeclarations = ''

      const readStream = createReadStream(fileLocation, 'utf8')

      readStream
        .on('data', (chunk) => {
          reduceDeclarations += chunk
        })
        .on('end', () => {
          let declarations
          try {
            declarations = JSON.parse(reduceDeclarations)
          } catch (e) {
            eventEmitter.emit('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_ERROR')
            handleOptionsFileReadError(e, fileLocation)
          }

          const {
            declaration_names: declarationNames = [],
            selectors = {}
          } = declarations

          const SELECTOR_PROPERTIES = new Map()
          let SELECTORS = []
          let DECLARATION_NAMES = [
            ...DEFAULT_DECLARATION_NAMES
          ]

          switch (typeof selectors) {
            case 'object':
              Object
                .entries(selectors)
                .forEach(([selector, properties]) => {
                  SELECTOR_PROPERTIES.set(selector, properties.replace(/^\s+|\s+$/g, '').replace(/\r?\n|\r/g, '').split(',').map(toTrim).filter(Boolean))
                })

              SELECTORS = Array.from(SELECTOR_PROPERTIES.keys())
              break
            case 'string':
              SELECTORS = (
                selectors.length
                  ? selectors.replace(/^\s+|\s+$/g, '').replace(/\r?\n|\r/g, '').split(',').map(toTrim).filter(Boolean)
                  : []
              )
              break
          }

          // by name
          if (typeof declarationNames === 'string') {
            DECLARATION_NAMES = declarationNames.replace(/^\s+|\s+$/g, '').split(',').map(toTrim).filter(Boolean)
          } else {
            if (Array.isArray(declarationNames)) {
              DECLARATION_NAMES = declarationNames.map(toTrim).filter(Boolean)
            }
          }

          PARAMS.selector_properties = SELECTOR_PROPERTIES
          PARAMS.selectors = SELECTORS
          PARAMS.declaration_names = DECLARATION_NAMES

          HAS_READ_REDUCE_DECLARATIONS = true

          eventEmitter.emit('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_END', OPTIONS)
        })
        .on('error', (e) => {
          eventEmitter.emit('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_ERROR', OPTIONS)
          handleOptionsFileReadError(e, DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION)
        })
    } // end of readReduceDeclarationsFileLocation

    function prepareSelectorsForHTML (selectors = [], html = null, options = null) {
      info('Prepare selectors for HTML')

      if (options) Object.assign(OPTIONS.html, options)

      html = html ?? OPTIONS.fileData.join('')
      delete OPTIONS.fileData

      let results = []
      let matches

      // find values in ids
      matches = html.match(/\bid\b[=](["'])(?:(?=(\\?))\2.)*?\1/gm)
      if (matches) {
        results = results.concat(
          Array.from(
            new Set(
              matches
                .filter(Boolean)
                .map((match) => match.split('=')[1].replace(/['"]+/g, ''))
            )
          )
        )
      }

      // find values in classes
      matches = html.match(/\bclass\b[=](["'])(?:(?=(\\?))\2.)*?\1/gm)
      if (matches) {
        results = results.concat(
          Array.from(
            new Set(
              matches
                .filter(Boolean)
                .map((match) => match.split('=')[1].replace(/['"]+/g, ''))
            )
          )
        )
      }

      // find values in internal selectors
      matches = html.match(/<style([\S\s]*?)>([\S\s]*?)<\/style>/gi)
      if (matches) {
        results = results.concat(
          Array.from(
            new Set(
              matches
                .filter(Boolean)
                .map((match) => match.split('</')[0].split('>')[1])
                .map((match) => match.match(/([#}.])([^0-9])([\S\s]*?){/g))
                .filter(Boolean)
                .map((match) => match.replace(/\r?\n|\r|\s/g, '').replace('{', '').replace('}', ''))
                .map((match) => match.split(',').map(toTrim).filter(Boolean))
            )
          )
        )
      }

      // find values in the dom
      const {
        window: {
          document
        }
      } = new JSDOM(html, { contentType: 'text/html' })

      selectors
        .forEach((selector, i) => {
          results
            .forEach((result) => {
              if (selector === result) selectors.splice(i, 1)
            })
        })

      selectors
        .forEach((selector, i) => {
          if (document.querySelector(selector)) selectors.splice(i, 1)
        })

      return selectors
    } // end of prepareSelectorsForHTML

    function processSelectorsForHTML (rules = [], selectors = []) {
      info('Process selectors for HTML')

      removeUnused(rules, selectors)

      processRules(rules, OPTIONS, SUMMARY, PARAMS)

      // after
      STATS.after.totalFileSizeKB = 0
      STATS.after.noNodes = 0
      STATS.after.noRules = 0
      STATS.after.noDeclarations = 0
      STATS.after.noComments = 0
      STATS.after.noCharset = 0
      STATS.after.noCustomMedia = 0
      STATS.after.noDocument = 0
      STATS.after.noFontFace = 0
      STATS.after.noHost = 0
      STATS.after.noImport = 0
      STATS.after.noKeyframes = 0
      STATS.after.noKeyframe = 0
      STATS.after.noMedia = 0
      STATS.after.noNamespace = 0
      STATS.after.noPage = 0
      STATS.after.noSupports = 0
      STATS.after.noNodes = rules.length

      rules
        .filter(Boolean)
        .forEach(getSummaryStatsFor(SUMMARY.stats.after))

      SUMMARY.stats.summary.noReductions.noRules = SUMMARY.stats.before.noRules - SUMMARY.stats.after.noRules
      SUMMARY.stats.summary.noReductions.noDeclarations = SUMMARY.stats.before.noDeclarations - SUMMARY.stats.after.noDeclarations
      SUMMARY.stats.summary.noReductions.noComments = SUMMARY.stats.before.noComments - SUMMARY.stats.after.noComments
      SUMMARY.stats.summary.noReductions.noCharset = SUMMARY.stats.before.noCharset - SUMMARY.stats.after.noCharset
      SUMMARY.stats.summary.noReductions.noCustomMedia = SUMMARY.stats.before.noCustomMedia - SUMMARY.stats.after.noCustomMedia
      SUMMARY.stats.summary.noReductions.noDocument = SUMMARY.stats.before.noDocument - SUMMARY.stats.after.noDocument
      SUMMARY.stats.summary.noReductions.noFontFace = SUMMARY.stats.before.noFontFace - SUMMARY.stats.after.noFontFace
      SUMMARY.stats.summary.noReductions.noHost = SUMMARY.stats.before.noHost - SUMMARY.stats.after.noHost
      SUMMARY.stats.summary.noReductions.noImport = SUMMARY.stats.before.noImport - SUMMARY.stats.after.noImport
      SUMMARY.stats.summary.noReductions.noKeyframes = SUMMARY.stats.before.noKeyframes - SUMMARY.stats.after.noKeyframes
      SUMMARY.stats.summary.noReductions.noKeyframe = SUMMARY.stats.before.noKeyframe - SUMMARY.stats.after.noKeyframe
      SUMMARY.stats.summary.noReductions.noMedia = SUMMARY.stats.before.noMedia - SUMMARY.stats.after.noMedia
      SUMMARY.stats.summary.noReductions.noNamespace = SUMMARY.stats.before.noNamespace - SUMMARY.stats.after.noNamespace
      SUMMARY.stats.summary.noReductions.noPage = SUMMARY.stats.before.noPage - SUMMARY.stats.after.noPage
      SUMMARY.stats.summary.noReductions.noSupports = SUMMARY.stats.before.noSupports - SUMMARY.stats.after.noSupports
      SUMMARY.stats.summary.noReductions.noNodes = SUMMARY.stats.before.noNodes - SUMMARY.stats.after.noNodes

      return cssTools.stringify({
        type: 'stylesheet',
        stylesheet: {
          rules
        }
      })
    } // end of processSelectorsForHTML

    async function processHTML (selectors = [], html = null, options = null) {
      info('Process HTML')

      // read html files
      if (OPTIONS.html && OPTIONS.special_reduce_with_html) {
        let {
          html: htmlFiles
        } = OPTIONS

        // check for file or files
        switch (typeof htmlFiles) {
          case 'object':
            {
              const collector = []

              Object
                .values(htmlFiles)
                .forEach((htmlFile) => {
                  getFilePath(htmlFile, ['.html', '.htm'], collector)
                })

              if (collector.length) {
                htmlFiles = collector
              }
            }
            break
          case 'array':
            {
              const collector = []

              htmlFiles
                .forEach((htmlFile) => {
                  getFilePath(htmlFile, ['.html', '.htm'], collector)
                })

              if (collector.length) {
                htmlFiles = collector
              }
            }
            break
          case 'string': // formats
            {
              let collection = htmlFiles.replace(/ /g, '') // comma delimited list - filename1.html, filename2.html
              if (collection.includes(',')) {
                collection = collection.replace(/^\s+|\s+$/g, '').split(',').map(toTrim).filter(Boolean)

                const collector = []

                collection
                  .forEach((member) => {
                    getFilePath(member, ['.html', '.htm'], collector)
                  })

                if (collector.length) {
                  htmlFiles = collector
                }
              } else {
                const collector = []

                // string path
                getFilePath(collection, ['.html', '.htm'], collector)

                if (collector.length) {
                  htmlFiles = collector
                }
              }
            }
            break
        } // end of switch

        eventEmitter
          .on('HTML_READ_AGAIN', async (fileIndex, fileData) => {
            prepareSelectorsForHTML(selectors, html, { ...options, fileData })

            await readHTMLFiles(htmlFiles, fileIndex, fileData)
          })
          .on('HTML_READ_END', (fileData) => {
            prepareSelectorsForHTML(selectors, html, { ...options, fileData })

            eventEmitter.emit('HTML_RESULTS_END', selectors)
          })

        await readHTMLFiles(htmlFiles)
      } // end of html files check
    } // end of processHTML

    async function readHTMLFiles (files = [], fileIndex = 0, fileData = []) {
      info('Read HTML files')

      const file = files[fileIndex]

      log(file)

      if (validUrl.isUri(file)) {
        try {
          const response = await fetch(file)
          const content = await response.text()
          const fileSizeKB = getSizeInKB(content)

          STATS.files.html.push({
            fileName: file,
            fileSizeKB
          })

          SUMMARY.files.input_html.push(file)

          fileData.push(content)

          const nextFileIndex = fileIndex + 1

          if (nextFileIndex < files.length) {
            eventEmitter.emit('HTML_READ_AGAIN', nextFileIndex, fileData)
          } else {
            eventEmitter.emit('HTML_READ_END', fileData)
          }
        } catch (e) {
          eventEmitter.emit('HTML_READ_ERROR')
          handleHtmlFileReadError(e, file)
        }
      } else {
        const fileSizeKB = getFileSizeInKB(file)

        STATS.files.html.push({
          fileName: file,
          fileSizeKB
        })

        SUMMARY.files.input_html.push(file)

        const readHTMLStream = createReadStream(file, 'utf8')

        readHTMLStream
          .on('data', (chunk) => {
            fileData.push(chunk)
          })
          .on('end', () => {
            const nextFileIndex = fileIndex + 1

            if (nextFileIndex < files.length) {
              eventEmitter.emit('HTML_READ_AGAIN', nextFileIndex, fileData)
            } else {
              eventEmitter.emit('HTML_READ_END', fileData)
            }
          })
          .on('error', (e) => {
            eventEmitter.emit('HTML_READ_ERROR')
            handleHtmlFileReadError(e, file)
          })
      }
    } // end of readHTMLFiles

    function processCSS (css = null, options = null, complete = () => {}) {
      info('Process CSS')

      function handleDefaultOptionReduceDeclarationsEnd () {
        eventEmitter.removeListener('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_END', handleDefaultOptionReduceDeclarationsEnd)

        if (css) {
          const fileSizeKB = getSizeInKB(css)

          STATS.before.totalFileSizeKB += fileSizeKB
        }

        // options
        if (options) Object.assign(OPTIONS, options)

        if (!OPTIONS.css) {
          const {
            file_path: FILE_PATH = DEFAULT_FILE_LOCATION
          } = OPTIONS

          OPTIONS.css = [FILE_PATH]
        }

        css = css ?? OPTIONS.fileData.join('')
        delete OPTIONS.fileData

        const {
          _3tokenValues,
          _4tokenValues,
          _5tokenValues,
          _6tokenValues,
          _7tokenValues,
          tokenComments
        } = getTokens()

        // tokens - allow multi-keyframe selectors
        css = css.replace(/(@(-?)[a-zA-Z\-]*(keyframes)*\s[a-zA-Z\-]*(\s*,?\s*)){2,}/g, (match) => { // eslint-disable-line no-useless-escape
          _7tokenValues.push(match)
          return '@keyframes _7token_' + _7tokenValues.length + ''
        })

        // tokens - data:image
        css = css.replace(/url\(\"data:image\/([a-zA-Z]*);base64,([^\"]*)\"\)/g, (match) => { // eslint-disable-line no-useless-escape
          _6tokenValues.push(match)
          return '_6token_dataimage_' + _6tokenValues.length + ':'
        })

        // remove non-standard commented lines
        css = css.replace(/([^(:;,a-zA-Z0-9]|^)\/\/.*$/gm, (match) => {
          STATS.summary.noInlineCommentsTrimmed += 1

          if (OPTIONS.trim_keep_non_standard_inline_comments && OPTIONS.trim_comments !== true) {
            return '/*' + match.substring(3, match.length) + ' */'
          } else {
            return ''
          }
        })

        // hacks - **/
        css = css.replace(/\/\*\*\//gm, '_1token_hck')

        // hacks - *\**/
        css = css.replace(/\/\*\\\*\*\//gm, '_2token_hck')

        // hacks - (specialchar)property
        css = css.replace(/[\!\$\&\*\(\)\=\%\+\@\,\.\/\`\[\]\#\~\?\:\<\>\|\*\/]{1}([\-\_\.]?)([a-zA-Z0-9]+):((\s\S*?));/g, (match) => { // eslint-disable-line no-useless-escape
          _3tokenValues.push(match.substring(0, match.length - 1))
          return '_3token_hck_' + _3tokenValues.length + ':'
        })

        // hacks - (;
        css = css.replace(/(\(;)([\s\S]*?)(\})/g, (match) => {
          _4tokenValues.push(match)
          return '_4token_hck_' + _4tokenValues.length + ':}'
        })

        // hacks - [;
        css = css.replace(/(\[;)([\s\S]*?)(\})/g, (match) => {
          _5tokenValues.push(match)
          return '_5token_hck_' + _5tokenValues.length + ':}'
        })

        // tokens - replace side comments
        if (OPTIONS.trim_comments !== true) {
          css = css.replace(/[;]([^\n][\s]*?)\/\*([\s\S]*?)\*\//gm, (match) => {
            const i = Object.keys(tokenComments).length + 1
            const k = '_cssp_sc' + i
            tokenComments[k] = match
            return `; /*_cssp_sc${i}*/`
          })
        }

        const {
          file_path: FILE_PATH = DEFAULT_FILE_LOCATION
        } = OPTIONS

        let ast
        try {
          ast = cssTools.parse(css, { source: FILE_PATH })
        } catch (e) {
          handleCssParseError(e)
        }

        const {
          stylesheet: {
            rules
          }
        } = ast

        SUMMARY.stats = STATS

        STATS.before.noNodes = rules.length

        rules
          .filter(Boolean)
          .forEach(getSummaryStatsFor(SUMMARY.stats.before))

        processRules(rules, OPTIONS, SUMMARY, PARAMS)
        processValues(rules, OPTIONS, SUMMARY)

        // @media rules
        rules
          .filter(Boolean)
          .filter(hasTypeMedia)
          .forEach(({ rules, media }) => {
            log(`@media ${media}`)

            processRules(rules, OPTIONS, SUMMARY, PARAMS)
            processValues(rules, OPTIONS, SUMMARY)
          })

        // @document rules
        if (!OPTIONS.bypass_document_rules) {
          rules
            .filter(Boolean)
            .filter(hasTypeDocument)
            .forEach(({ rules, document }) => {
              log(`@document ${document}`)

              processRules(rules, OPTIONS, SUMMARY, PARAMS)
              processValues(rules, OPTIONS, SUMMARY)
            })
        }

        // @supports rules
        if (!OPTIONS.bypass_supports_rules) {
          rules
            .filter(Boolean)
            .filter(hasTypeSupports)
            .forEach(({ rules, supports }) => {
              log(`@supports ${supports}`)

              processRules(rules, OPTIONS, SUMMARY, PARAMS)
              processValues(rules, OPTIONS, SUMMARY)
            })
        }

        // charset rules
        if (!OPTIONS.bypass_charset) {
          rules
            .forEach((rule, i) => {
              if (rule) {
                const {
                  type
                } = rule

                if (type === 'charset') {
                  const {
                    charset: ALPHA = ''
                  } = rule

                  rules.slice(i)
                    .forEach((rule, j) => {
                      if (rule) {
                        const {
                          type
                        } = rule

                        if (type === 'charset') {
                          const {
                            charset: OMEGA = ''
                          } = rule

                          if (ALPHA === OMEGA) {
                            rules.splice(j, 1) // remove charset

                            const siblingRule = rules[j] // rules.slice(j).shift()
                            if (siblingRule) {
                              const {
                                type,
                                comment
                              } = siblingRule

                              if (
                                type === 'comment' &&
                                comment.includes('_cssp_sc')
                              ) {
                                rules.splice(j, 1) // remove comment
                              }
                            }
                          }
                        }
                      }
                    })

                  if (!(ALPHA.startsWith('"') && ALPHA.endsWith('"'))) {
                    rules.splice(i, 1) // remove charset

                    const siblingRule = rules[i] // rules.slice(i).shift()
                    if (siblingRule) {
                      const {
                        type,
                        comment
                      } = siblingRule

                      if (
                        type === 'comment' &&
                        comment.includes('_cssp_sc')
                      ) {
                        rules.splice(i, 1) // remove comment
                      }
                    }
                  }
                }
              }
            })
        }

        const {
          special_convert_rem: SPECIAL_CONVERT_REM
        } = OPTIONS

        // rems - html check
        if (SPECIAL_CONVERT_REM) {
          const {
            special_convert_rem_px: SPECIAL_CONVERT_REM_PX,
            special_convert_rem_default_px: SPECIAL_CONVERT_REM_DEFAULT_PX
          } = OPTIONS

          const remPx = Number(SPECIAL_CONVERT_REM_PX)
          const remDefaultPx = Number(SPECIAL_CONVERT_REM_DEFAULT_PX)
          const FONT_SIZE = ((remPx / remDefaultPx) * 100) + '%'

          if (rules.some(hasHtml)) {
            rules
              .forEach((rule, i, rules) => {
                const {
                  selectors = []
                } = rule

                /**
                 *  Has
                 */
                if (selectors.some((selector) => selector.includes('html'))) {
                  const {
                    declarations = []
                  } = rule

                  /**
                   *  Has not
                   */
                  if (!declarations.some(({ property }) => property === 'font-size')) {
                    /**
                     *  Add to the start
                     */
                    declarations.unshift({
                      type: 'declaration',
                      property: 'font-size',
                      value: FONT_SIZE
                    })

                    /**
                     *  Put at the start
                     */
                    rules.unshift(rules.splice(i, 1).shift())
                  }
                }
              })
          } else {
            rules.unshift({
              type: 'rule',
              selectors: ['html'],
              declarations: [
                {
                  type: 'declaration',
                  property: 'font-size',
                  value: FONT_SIZE
                }
              ]
            })
          }
        } // end of rems - html check

        /// charset check
        if (!OPTIONS.bypass_charset) {
          if (rules.length >= 2) {
            const [
              RULE_ONE,
              RULE_TWO
            ] = rules

            if (
              RULE_ONE.type === 'comment' &&
              RULE_TWO.type === 'charset'
            ) {
              rules.splice(0, 1)
            }
          }
        }
        /// end of charset check

        // after
        STATS.after.noNodes = rules.length

        rules
          .filter(Boolean)
          .forEach(getSummaryStatsFor(SUMMARY.stats.after))

        SUMMARY.stats.summary.noReductions.noRules = SUMMARY.stats.before.noRules - SUMMARY.stats.after.noRules
        SUMMARY.stats.summary.noReductions.noDeclarations = SUMMARY.stats.before.noDeclarations - SUMMARY.stats.after.noDeclarations
        SUMMARY.stats.summary.noReductions.noComments = SUMMARY.stats.before.noComments - SUMMARY.stats.after.noComments
        SUMMARY.stats.summary.noReductions.noCharset = SUMMARY.stats.before.noCharset - SUMMARY.stats.after.noCharset
        SUMMARY.stats.summary.noReductions.noCustomMedia = SUMMARY.stats.before.noCustomMedia - SUMMARY.stats.after.noCustomMedia
        SUMMARY.stats.summary.noReductions.noDocument = SUMMARY.stats.before.noDocument - SUMMARY.stats.after.noDocument
        SUMMARY.stats.summary.noReductions.noFontFace = SUMMARY.stats.before.noFontFace - SUMMARY.stats.after.noFontFace
        SUMMARY.stats.summary.noReductions.noHost = SUMMARY.stats.before.noHost - SUMMARY.stats.after.noHost
        SUMMARY.stats.summary.noReductions.noImport = SUMMARY.stats.before.noImport - SUMMARY.stats.after.noImport
        SUMMARY.stats.summary.noReductions.noKeyframes = SUMMARY.stats.before.noKeyframes - SUMMARY.stats.after.noKeyframes
        SUMMARY.stats.summary.noReductions.noKeyframe = SUMMARY.stats.before.noKeyframe - SUMMARY.stats.after.noKeyframe
        SUMMARY.stats.summary.noReductions.noMedia = SUMMARY.stats.before.noMedia - SUMMARY.stats.after.noMedia
        SUMMARY.stats.summary.noReductions.noNamespace = SUMMARY.stats.before.noNamespace - SUMMARY.stats.after.noNamespace
        SUMMARY.stats.summary.noReductions.noPage = SUMMARY.stats.before.noPage - SUMMARY.stats.after.noPage
        SUMMARY.stats.summary.noReductions.noSupports = SUMMARY.stats.before.noSupports - SUMMARY.stats.after.noSupports
        SUMMARY.stats.summary.noReductions.noNodes = SUMMARY.stats.before.noNodes - SUMMARY.stats.after.noNodes

        // prepare output
        let outputCSS = cssTools.stringify({
          type: 'stylesheet',
          stylesheet: {
            rules
          }
        })

        // Detect via JS
        // Detect via HTML
        if (OPTIONS.html && OPTIONS.special_reduce_with_html) {
          const {
            file_path: FILE_PATH = DEFAULT_FILE_LOCATION
          } = OPTIONS

          let ast
          try {
            ast = cssTools.parse(outputCSS, { source: FILE_PATH })
          } catch (e) {
            handleCssParseError(e)
          }

          const {
            stylesheet: {
              rules
            }
          } = ast

          let selectors = []

          const {
            special_reduce_with_html_ignore_selectors: SPECIAL_REDUCE_WITH_HTML_IGNORE_SELECTORS
          } = OPTIONS

          rules
            .filter(Boolean)
            .forEach((rule) => {
              const { type } = rule
              if (type === 'rule') {
                getSelectors(rule, selectors, SPECIAL_REDUCE_WITH_HTML_IGNORE_SELECTORS)
              } else {
                if (
                  type === 'media' ||
                  type === 'document' ||
                  type === 'supports'
                ) {
                  rule.rules
                    .forEach((rule) => {
                      getSelectors(rule, selectors, SPECIAL_REDUCE_WITH_HTML_IGNORE_SELECTORS)
                    })
                }
              }
            })

          // remove duplicates
          selectors = Array.from(new Set(selectors))

          // process selectors returned from processing HTML
          eventEmitter
            .on('HTML_RESULTS_END', (selectorsRemoved) => {
              SUMMARY.selectors_removed = selectorsRemoved

              let css = processSelectorsForHTML(rules, selectors)

              const {
                css_file_location: CSS_FILE_LOCATION
              } = OPTIONS

              if (CSS_FILE_LOCATION) css = writeCSSFiles(css, CSS_FILE_LOCATION)
              else {
                css = trim(css, OPTIONS, SUMMARY)
                css = hack(css, OPTIONS, SUMMARY, getTokens())
                const fileSizeKB = getSizeInKB(css) // getSizeInKB(css) / 1000
                SUMMARY.stats.after.totalFileSizeKB += fileSizeKB
              }

              SUMMARY.stats.summary.savingsKB = roundTo(SUMMARY.stats.before.totalFileSizeKB - SUMMARY.stats.after.totalFileSizeKB, 4)
              SUMMARY.stats.summary.savingsPercentage = roundTo(SUMMARY.stats.summary.savingsKB / SUMMARY.stats.before.totalFileSizeKB * 100, 2)

              complete(null, css)

              const {
                report: REPORT
              } = OPTIONS

              if (REPORT) {
                const {
                  report_file_location: REPORT_FILE_LOCATION = DEFAULT_OPTIONS_REPORT_FILE_LOCATION
                } = OPTIONS

                try {
                  SUMMARY.options = (
                    Object.fromEntries(
                      Object
                        .entries(OPTIONS)
                        .sort(([alpha], [omega]) => alpha.localeCompare(omega))
                    )
                  )

                  writeFileSync(REPORT_FILE_LOCATION, JSON.stringify(SUMMARY, null, 2))
                } catch (e) {
                  handleOptionsFileWriteError(e, REPORT_FILE_LOCATION)
                }
              }

              const {
                verbose: VERBOSE
              } = OPTIONS

              if (VERBOSE) {
                console.table({
                  Before: {
                    KB: roundTo(SUMMARY.stats.before.totalFileSizeKB, 4)
                  },
                  After: {
                    KB: roundTo(SUMMARY.stats.after.totalFileSizeKB, 4)
                  },
                  Saved: {
                    KB: roundTo(SUMMARY.stats.summary.savingsKB, 4),
                    '%': roundTo(SUMMARY.stats.summary.savingsPercentage, 2)
                  }
                })
              }
            })

          processHTML(selectors)
        } else { // end of special_reduce_with_html
          const {
            css_file_location: CSS_FILE_LOCATION
          } = OPTIONS

          if (CSS_FILE_LOCATION) outputCSS = writeCSSFiles(outputCSS, CSS_FILE_LOCATION)
          else {
            outputCSS = trim(outputCSS, OPTIONS, SUMMARY)
            outputCSS = hack(outputCSS, OPTIONS, SUMMARY, getTokens())
            const fileSizeKB = getSizeInKB(outputCSS) // getSizeInKB(css) / 1000
            SUMMARY.stats.after.totalFileSizeKB += fileSizeKB
          }

          SUMMARY.stats.summary.savingsKB = roundTo(SUMMARY.stats.before.totalFileSizeKB - SUMMARY.stats.after.totalFileSizeKB, 4)
          SUMMARY.stats.summary.savingsPercentage = roundTo(SUMMARY.stats.summary.savingsKB / SUMMARY.stats.before.totalFileSizeKB * 100, 2)

          complete(null, outputCSS)

          const {
            report: REPORT
          } = OPTIONS

          if (REPORT) {
            const {
              report_file_location: REPORT_FILE_LOCATION = DEFAULT_OPTIONS_REPORT_FILE_LOCATION
            } = OPTIONS

            try {
              SUMMARY.options = (
                Object.fromEntries(
                  Object
                    .entries(OPTIONS)
                    .sort(([alpha], [omega]) => alpha.localeCompare(omega))
                )
              )

              writeFileSync(REPORT_FILE_LOCATION, JSON.stringify(SUMMARY, null, 2))
            } catch (e) {
              handleOptionsFileWriteError(e, REPORT_FILE_LOCATION)
            }
          }

          const {
            verbose: VERBOSE
          } = OPTIONS

          if (VERBOSE) {
            console.table({
              Before: {
                KB: roundTo(SUMMARY.stats.before.totalFileSizeKB, 4)
              },
              After: {
                KB: roundTo(SUMMARY.stats.after.totalFileSizeKB, 4)
              },
              Saved: {
                KB: roundTo(SUMMARY.stats.summary.savingsKB, 4),
                '%': roundTo(SUMMARY.stats.summary.savingsPercentage, 2)
              }
            })
          }
        } // end of special_reduce_with_html
      }

      eventEmitter.on('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_END', handleDefaultOptionReduceDeclarationsEnd) // end of event

      if (!css) eventEmitter.emit('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_END')

      if (!HAS_READ_REDUCE_DECLARATIONS) {
        const REDUCE_DECLARATIONS_FILE_LOCATION = OPTIONS.reduce_declarations_file_location

        const reduceDeclarationsFileLocation = (
          DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION === path.join(ROOT, REDUCE_DECLARATIONS_FILE_LOCATION)
            ? DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION
            : REDUCE_DECLARATIONS_FILE_LOCATION
        )

        OPTIONS.reduce_declarations_file_location = reduceDeclarationsFileLocation

        if (existsSync(reduceDeclarationsFileLocation)) {
          readReduceDeclarationsFileLocation(reduceDeclarationsFileLocation)
        } else {
          if (options && !options.reduce_declarations) {
            const reduceDeclarations = {
              declaration_names: [
                ...DEFAULT_DECLARATION_NAMES
              ]
            }

            options.reduce_declarations = reduceDeclarations
            options.reduce_declarations_file_location = reduceDeclarationsFileLocation

            readReduceDeclarations(reduceDeclarations)
          } else {
            readReduceDeclarationsFileLocation(DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION)
          }
        }
      }
    } // end of processCSS

    function processCSSFiles (options = INITIAL_OPTIONS, fileLocation = DEFAULT_OPTIONS_FILE_LOCATION, complete) {
      info('Process CSS files')

      async function handleDefaultOptionsReadEnd () {
        eventEmitter.removeListener('DEFAULT_OPTIONS_READ_END', handleDefaultOptionsReadEnd)

        // options (css files)
        if (!OPTIONS.css) OPTIONS.css = INITIAL_OPTIONS.css

        // options
        if (options) Object.assign(OPTIONS, options)

        let {
          css: cssFiles
        } = OPTIONS

        if (cssFiles) {
          // check for file or files
          switch (typeof cssFiles) {
            case 'array':
              {
                const collector = []

                cssFiles
                  .forEach((file) => {
                    getFilePath(file, ['.css'], collector)
                  })

                if (collector.length) {
                  cssFiles = collector
                }
              }

              break
            case 'string':
              {
                // formats
                let collection = cssFiles.replace(/ /g, '')
                if (collection.includes(',')) { // comma delimited list - filename1.css, filename2.css
                  collection = collection.split(',').map(toTrim).filter(Boolean)

                  const collector = []

                  collection
                    .forEach((member) => {
                      getFilePath(member, ['.css'], collector)
                    })

                  if (collector.length) {
                    cssFiles = collector
                  }
                } else {
                  const collector = []

                  // string path
                  getFilePath(collection, ['.css'], collector)

                  if (collector.length) {
                    cssFiles = collector
                  }
                }
              }

              break
          } // end of switch

          fileLocation = cssFiles.toString()

          eventEmitter
            .on('CSS_READ_AGAIN', async (fileIndex, fileData) => {
              await readCSSFiles(cssFiles, fileIndex, fileData)
            })
            .on('CSS_READ_END', (fileData) => {
              processCSS(null, { ...OPTIONS, fileData }, complete)
            })

          await readCSSFiles(cssFiles)
        }
      }

      function handleDefaultOptionsReduceDeclarationsEnd () {
        eventEmitter.removeListener('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_END', handleDefaultOptionsReduceDeclarationsEnd)

        if (OPTIONS_FILE_LOCATION !== fileLocation) { // don't read same config
          eventEmitter.on('DEFAULT_OPTIONS_READ_END', handleDefaultOptionsReadEnd) // end of config read
        }

        OPTIONS_FILE_LOCATION = fileLocation

        if (fileLocation !== 'cmd_default') {
          readOptionsFileLocation(fileLocation)
        } else {
          if (fileLocation === 'cmd_default') {
            readOptions(options)
          }
        }
      }

      eventEmitter.on('DEFAULT_OPTIONS_REDUCE_DECLARATIONS_END', handleDefaultOptionsReduceDeclarationsEnd) // end of reduce config read

      if (!HAS_READ_REDUCE_DECLARATIONS) {
        const REDUCE_DECLARATIONS_FILE_LOCATION = OPTIONS.reduce_declarations_file_location

        const reduceDeclarationsFileLocation = (
          DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION === path.join(ROOT, REDUCE_DECLARATIONS_FILE_LOCATION)
            ? DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION
            : REDUCE_DECLARATIONS_FILE_LOCATION
        )

        OPTIONS.reduce_declarations_file_location = reduceDeclarationsFileLocation

        if (existsSync(reduceDeclarationsFileLocation)) {
          readReduceDeclarationsFileLocation(reduceDeclarationsFileLocation)
        } else {
          if (options && !options.reduce_declarations) {
            const reduceDeclarations = {
              declaration_names: [
                ...DEFAULT_DECLARATION_NAMES
              ]
            }

            options.reduce_declarations = reduceDeclarations
            options.reduce_declarations_file_location = reduceDeclarationsFileLocation

            readReduceDeclarations(reduceDeclarations)
          } else {
            readReduceDeclarationsFileLocation(DEFAULT_OPTIONS_REDUCE_DECLARATIONS_FILE_LOCATION)
          }
        }
      }
    } // end of processCSSFiles

    async function readCSSFiles (files = [], fileIndex = 0, fileData = []) {
      info('Read CSS files')

      const file = files[fileIndex]

      log(file)

      if (validUrl.isUri(file)) {
        try {
          const response = await fetch(file)
          const content = await response.text()
          const fileSizeKB = getSizeInKB(content)

          STATS.files.css.push({
            fileName: file,
            fileSizeKB
          })
          STATS.before.totalFileSizeKB += fileSizeKB

          SUMMARY.files.input_css.push(file)

          fileData.push(content)

          const nextFileIndex = fileIndex + 1

          if (nextFileIndex < files.length) {
            eventEmitter.emit('CSS_READ_AGAIN', nextFileIndex, fileData)
          } else {
            eventEmitter.emit('CSS_READ_END', fileData)
          }
        } catch (e) {
          eventEmitter.emit('CSS_READ_ERROR')
          handleCssFileReadError(e, file)
        }
      } else {
        const fileSizeKB = getFileSizeInKB(file)

        STATS.files.css.push({
          fileName: file,
          fileSizeKB
        })
        STATS.before.totalFileSizeKB += fileSizeKB

        SUMMARY.files.input_css.push(file)

        const readStream = createReadStream(file, 'utf8')

        readStream
          .on('data', (chunk) => {
            fileData.push(chunk)
          })
          .on('end', () => {
            const nextFileIndex = fileIndex + 1

            if (nextFileIndex < files.length) {
              eventEmitter.emit('CSS_READ_AGAIN', nextFileIndex, fileData)
            } else {
              eventEmitter.emit('CSS_READ_END', fileData)
            }
          })
          .on('error', (e) => {
            eventEmitter.emit('CSS_READ_ERROR')
            handleCssFileReadError(e, file)
          })
      }
    } // end of readCSSFiles

    function writeCSSFiles (css = '', fileLocation) {
      info('Write CSS files')

      try {
        let fileSizeKB = 0

        const directoryPath = path.dirname(fileLocation)
        const {
          name
        } = path.parse(fileLocation)

        const {
          format_group_limit: FORMAT_GROUP_LIMIT
        } = OPTIONS

        if (FORMAT_GROUP_LIMIT) {
          if (Math.ceil(SUMMARY.stats.after.noRules / FORMAT_GROUP_LIMIT) > 1) {
            let ast
            try {
              ast = cssTools.parse(css, { source: fileLocation })
            } catch (e) {
              handleCssParseError(e)
            }

            const {
              stylesheet: {
                rules
              }
            } = ast

            toGroups(rules, FORMAT_GROUP_LIMIT)
              .forEach((rules, i) => {
                /**
                   *  Redeclared so as not to modify `css` in scope
                   */
                let css = cssTools.stringify({
                  type: 'stylesheet',
                  stylesheet: {
                    rules
                  }
                })
                css = trim(css, OPTIONS, SUMMARY)
                css = hack(css, OPTIONS, SUMMARY, getTokens())
                const filePath = path.join(directoryPath, `${name}_${i}.css`)
                writeFileSync(filePath, css)
                fileSizeKB += getFileSizeInKB(filePath)
              })
          } else {
            css = trim(css, OPTIONS, SUMMARY)
            css = hack(css, OPTIONS, SUMMARY, getTokens())
            const filePath = path.join(directoryPath, name + '.css')
            writeFileSync(filePath, css)
            fileSizeKB = getFileSizeInKB(filePath)
          }
        } else {
          css = trim(css, OPTIONS, SUMMARY)
          css = hack(css, OPTIONS, SUMMARY, getTokens())
          const filePath = path.join(directoryPath, name + '.css')
          writeFileSync(filePath, css)
          fileSizeKB = getFileSizeInKB(filePath)
        }

        SUMMARY.stats.after.totalFileSizeKB += fileSizeKB
        return css
      } catch (e) {
        handleCssFileWriteError(e, fileLocation)
      }
    } // end of writeCSSFiles

    this.purgeCSS = function purgeCSS (css, options, complete) {
      processCSS(css, options, complete)
    }

    this.purgeCSSFiles = function purgeCSSFiles (options, fileLocation, complete) {
      processCSSFiles(options, fileLocation, complete)
    }
  }
} // end of CSSPurge

log('`css-purge` is awake')

export default new CSSPurge()