import { minifyJSON } from './helpers/json.util' ; import { minifyCSS } from './helpers/css.util' ; const postcss = require ( 'postcss' ) ; const fs = require ( 'fs-extra' ) ; const THEMIFY = 'themify' ; const JSToSass = require ( './helpers/js-sass' ) ; export interface ThemifyOptions { /**
     * Whether we would like to generate the CSS variables.
     * This should be true, unless you want to inject them yourself.
     */ createVars : boolean ; /**
     * Palette configuration
     */ palette : any ; /**
     * A class prefix to append to the generated themes classes
     */ classPrefix : string ; /**
     * Whether to generate a fallback for legacy browsers (ahm..ahm..) that do not supports CSS Variables
     */ screwIE11 : boolean ; /**
     * Legacy browser fallback
     */ fallback : { /**
         * An absolute path to the fallback CSS.
         */ cssPath : string | null ; /**
         * An absolute path to the fallback JSON.
         * This file contains variable that will be replace in runtime, for legacy browsers
         */ dynamicPath : string | null ; } ; } const defaultOptions = { createVars : true , palette : { } , classPrefix : '' , screwIE11 : true , fallback : { cssPath : null , dynamicPath : null } } ; /** supported color variations */ const ColorVariation = { DARK : 'dark' , LIGHT : 'light' } ; function buildOptions ( options ) { if ( ! options ) { throw new Error ( `options is required.` ) ; } if ( ! options . palette ) { throw new Error ( `The 'palette' option is required.` ) ; } return { ... defaultOptions , ... options } ; } param { string } filePath param { string } output returns { Promise < any > } function writeToFile ( filePath , output ) { return fs . outputFile ( filePath , output ) ; } param value function getRgbaNumbers ( value ) { return hexToRgba ( value ) . replace ( 'rgba(' , '' ) . replace ( ', 1)' , '' ) ; } /** Define the default variation */ const defaultVariation = ColorVariation . LIGHT ; /** An array of variation values  */ const variationValues = ( Object as any ) . values ( ColorVariation ) ; /** An array of all non-default variations */ const nonDefaultVariations = variationValues . filter ( v => v !== defaultVariation ) ; function themify ( options ) { /** Regex to get the value inside the themify parenthesis */ const themifyRegExp = /themify\(([^)]+)\)/gi ; /**
     * Define the method of color execution
     */ const enum ExecutionMode { CSS_VAR = 'CSS_VAR' , CSS_COLOR = 'CSS_COLOR' , DYNAMIC_EXPRESSION = 'DYNAMIC_EXPRESSION' } options = buildOptions ( options ) ; return root => { if ( options . screwIE11 === false ) { processFallbackRules ( root ) ; } processRules ( root ) ; } ; example example example example example function getThemifyValue ( propertyValue , execMode ) { /** Remove the start and end ticks **/ propertyValue = propertyValue . replace ( /'/g , '' ) ; const colorVariations = { } ; function normalize ( value , variationName ) { let parsedValue ; try { parsedValue = JSON . parse ( value ) ; } catch ( ex ) { throw new Error ( `fail to parse the following expression: ${ value }.` ) ; } const currentValue = parsedValue [ variationName ] ; /** For example: background-color: themify((light: primary-100)); */ if ( ! currentValue ) { throw new Error ( `${ value } has one variation.` ) ; } if ( ! Array . isArray ( currentValue ) ) { parsedValue [ variationName ] = [ currentValue , 1 ] ; } else if ( ! currentValue . length || ! currentValue [ 0 ] ) { throw new Error ( 'Oops. Received an empty color!' ) ; } if ( options . palette ) return parsedValue [ variationName ] ; } variationValues . forEach ( variationName => { colorVariations [ variationName ] = propertyValue . replace ( themifyRegExp , ( occurrence , value ) => { const parsedColor = normalize ( value , variationName ) ; return translateColor ( parsedColor , variationName , execMode ) ; } ) ; } ) ; return colorVariations ; } param colorArr param variationName param execMode function translateColor ( colorArr , variationName , execMode ) { const [ colorVar , alpha ] = colorArr ; const underlineColor = options . palette [ variationName ] [ colorVar ] ; if ( ! underlineColor ) { if ( variationName !== defaultVariation ) { return null ; } throw new Error ( `The variable name '${ colorVar }' doesn't exists in your palette.` ) ; } switch ( execMode ) { case ExecutionMode . CSS_COLOR : if ( alpha === '1' ) { return underlineColor ; } const rgbaColorArr = getRgbaNumbers ( underlineColor ) ; return `rgba(${ rgbaColorArr }, ${ alpha })` ; case ExecutionMode . DYNAMIC_EXPRESSION : return `%[${ variationName }, ${ colorVar }, ${ alpha }]%` ; default : return `rgba(var(--${ colorVar }), ${ alpha })` ; } } example param root function processRules ( root ) { root . walkRules ( rule => { if ( ! hasThemify ( rule . toString ( ) ) ) { return ; } let aggragatedSelectorsMap = { } ; let aggragatedSelectors = [ ] ; let createdRules = [ ] ; const variationRules = { [ defaultVariation ] : rule } ; rule . walkDecls ( decl => { const propertyValue = decl . value ; if ( ! hasThemify ( propertyValue ) ) return ; const property = decl . prop ; const variationValueMap = getThemifyValue ( propertyValue , ExecutionMode . CSS_VAR ) ; const defaultVariationValue = variationValueMap [ defaultVariation ] ; decl . value = defaultVariationValue ; const createNonDefaultVariationRules = isAtRule ( rule ) ; if ( createNonDefaultVariationRules ) { return ; } nonDefaultVariations . forEach ( variationName => { const currentValue = variationValueMap [ variationName ] ; if ( ! currentValue || currentValue === 'null' ) { return ; } if ( currentValue === defaultVariationValue ) { const selector = getSelectorName ( rule , variationName ) ; if ( ! aggragatedSelectorsMap [ variationName ] ) { aggragatedSelectorsMap [ variationName ] = true ; aggragatedSelectors . push ( selector ) ; } } else { if ( ! variationRules [ variationName ] ) { const clonedRule = createRuleWithVariation ( rule , variationName ) ; variationRules [ variationName ] = clonedRule ; createdRules . push ( clonedRule ) ; } const variationDecl = createDecl ( property , variationValueMap [ variationName ] ) ; variationRules [ variationName ] . append ( variationDecl ) ; } } ) ; } ) ; if ( aggragatedSelectors . length ) { rule . selectors = [ ... rule . selectors , ... aggragatedSelectors ] ; } if ( createdRules . length ) { createdRules . forEach ( r => root . append ( r ) ) ; } } ) ; } param rule return { boolean } function isAtRule ( rule ) { return rule . parent && rule . parent . type === 'atrule' ; } param root function processFallbackRules ( root ) { const output = { [ ExecutionMode . CSS_COLOR ] : [ ] , [ ExecutionMode . DYNAMIC_EXPRESSION ] : { } } ; variationValues . forEach ( variation => ( output [ ExecutionMode . DYNAMIC_EXPRESSION ] [ variation ] = [ ] ) ) ; const execModes = [ ExecutionMode . CSS_COLOR , ExecutionMode . DYNAMIC_EXPRESSION ] ; walkFallbackAtRules ( root , execModes , output ) ; walkFallbackRules ( root , execModes , output ) ; writeFallbackCSS ( output ) ; } function writeFallbackCSS ( output ) { if ( output [ ExecutionMode . CSS_COLOR ] . length ) { const fallbackCss = output [ ExecutionMode . CSS_COLOR ] . join ( '' ) ; writeToFile ( options . fallback . cssPath as string , minifyCSS ( fallbackCss ) ) ; const jsonOutput = { } ; variationValues . forEach ( variationName => { jsonOutput [ variationName ] = output [ ExecutionMode . DYNAMIC_EXPRESSION ] [ variationName ] || [ ] ; jsonOutput [ variationName ] = minifyJSON ( jsonOutput [ variationName ] . join ( '' ) ) ; jsonOutput [ variationName ] = minifyCSS ( jsonOutput [ variationName ] ) ; } ) ; const dynamicCss = JSON . stringify ( jsonOutput ) ; writeToFile ( options . fallback . dynamicPath as string , dynamicCss ) ; } } function walkFallbackAtRules ( root , execModes , output ) { root . walkAtRules ( atRule => { if ( atRule . nodes && hasThemify ( atRule . toString ( ) ) ) { execModes . forEach ( mode => { const clonedAtRule = atRule . clone ( ) ; clonedAtRule . nodes . forEach ( rule => { rule . walkDecls ( decl => { const propertyValue = decl . value ; if ( hasThemify ( propertyValue ) ) { const colorMap = getThemifyValue ( propertyValue , mode ) ; decl . value = colorMap [ defaultVariation ] ; } } ) ; } ) ; let rulesOutput = mode === ExecutionMode . DYNAMIC_EXPRESSION ? output [ mode ] [ defaultVariation ] : output [ mode ] ; rulesOutput . push ( clonedAtRule ) ; } ) ; } } ) ; } function walkFallbackRules ( root , execModes , output ) { root . walkRules ( rule => { if ( isAtRule ( rule ) || ! hasThemify ( rule . toString ( ) ) ) { return ; } const ruleModeMap = { } ; rule . walkDecls ( decl => { const propertyValue = decl . value ; if ( ! hasThemify ( propertyValue ) ) return ; const property = decl . prop ; execModes . forEach ( mode => { const colorMap = getThemifyValue ( propertyValue , mode ) ; if ( ! ruleModeMap . hasOwnProperty ( mode ) ) { ruleModeMap [ mode ] = { } ; variationValues . forEach ( variationName => { let newRule ; if ( variationName === defaultVariation ) { newRule = cloneEmptyRule ( rule ) ; } else { newRule = createRuleWithVariation ( rule , variationName ) ; } let rulesOutput = mode === ExecutionMode . DYNAMIC_EXPRESSION ? output [ mode ] [ variationName ] : output [ mode ] ; rulesOutput . push ( newRule ) ; ruleModeMap [ mode ] [ variationName ] = newRule ; } ) ; } variationValues . forEach ( variationName => { const underlineColor = colorMap [ variationName ] ; if ( underlineColor && underlineColor !== 'null' ) { const newDecl = createDecl ( property , colorMap [ variationName ] ) ; ruleModeMap [ mode ] [ variationName ] . append ( newDecl ) ; } } ) ; } ) ; } ) ; } ) ; } function createDecl ( prop , value ) { return postcss . decl ( { prop , value } ) ; } param propertyValue function hasThemify ( propertyValue ) { return propertyValue . indexOf ( THEMIFY ) > - 1 ; } param rule param variationName function createRuleWithVariation ( rule , variationName ) { const selector = getSelectorName ( rule , variationName ) ; return postcss . rule ( { selector } ) ; } param rule param variationName function getSelectorName ( rule , variationName ) { const selectorPrefix = `.${ options . classPrefix || '' }${ variationName }` ; return rule . selectors . map ( selector => { return `${ selectorPrefix } ${ selector }` ; } ) . join ( ',' ) ; } function cloneEmptyRule ( rule , overrideConfig ? ) { const clonedRule = rule . clone ( overrideConfig ) ; clonedRule . removeAll ( ) ; return clonedRule ; } } /**
 * Generating a SASS definition file with the palette map and the CSS variables.
 * This file should be injected into your bundle.
 */ function init ( options ) { options = buildOptions ( options ) ; return root => { const palette = options . palette ; const css = generateVars ( palette , options . classPrefix ) ; const parsedCss = postcss . parse ( css ) ; root . prepend ( parsedCss ) ; } ; /**
     * This function responsible for creating the CSS variable.
     *
     *  The output should look like the following:
     *
     *  .light {
         --primary-700: 255, 255, 255;
         --primary-600: 248, 248, 249;
         --primary-500: 242, 242, 244;
   *   }
     *
     *  .dark {
         --primary-700: 255, 255, 255;
         --primary-600: 248, 248, 249;
         --primary-500: 242, 242, 244;
   *   }
     *
     */ function generateVars ( palette , prefix ) { let cssOutput = '' ; prefix = prefix || '' ; Object . keys ( palette ) . forEach ( variationName => { const selector = variationName === ColorVariation . LIGHT ? ':root' : `.${ prefix }${ variationName }` ; const variationColors = palette [ variationName ] ; if ( ! variationColors ) { throw new Error ( `Expected map of colors for the variation name ${ variationName }` ) ; } const variationKeys = Object . keys ( variationColors ) ; const vars = variationKeys . map ( varName => { return `--${ varName }: ${ getRgbaNumbers ( variationColors [ varName ] ) };` ; } ) . join ( ' ' ) ; const output = `${ selector } {${ vars }}` ; cssOutput = `${ cssOutput } ${ output }` ; } ) ; cssOutput += `$palette: ${ JSToSass ( palette ) };` ; return cssOutput ; } } function hexToRgba ( hex , alpha = 1 ) { hex = hex . replace ( '#' , '' ) ; const r = parseInt ( hex . length == 3 ? hex . slice ( 0 , 1 ) . repeat ( 2 ) : hex . slice ( 0 , 2 ) , 16 ) ; const g = parseInt ( hex . length == 3 ? hex . slice ( 1 , 2 ) . repeat ( 2 ) : hex . slice ( 2 , 4 ) , 16 ) ; const b = parseInt ( hex . length == 3 ? hex . slice ( 2 , 3 ) . repeat ( 2 ) : hex . slice ( 4 , 6 ) , 16 ) ; return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')' ; } module . exports = { initThemify : postcss . plugin ( 'datoThemes' , init ) , themify : postcss . plugin ( 'datoThemes' , themify ) } ;