import * as fs from "fs-extra" ; import * as path from "path" ; import * as postcss from "postcss" ; import * as selectorParser from "postcss-selector-parser" ; import * as vars from "postcss-simple-vars" ; import { BemSelector , BlockClassSelector , SelectorBemObject } from "./interface" ; import { getBemNamesFromUser } from "./userInput" ; import { findLcsMap } from "./utils" ; export declare type PostcssAny = unknown ; type BemSelectorMap = Map < string , BemSelector > ; type ElementToBemSelectorMap = Map < string , BemSelector [ ] > ; type BlockToBemSelectorMap = Map < string , ElementToBemSelectorMap > ; type BemToBlockClassMap = WeakMap < BemSelector , BlockClassSelector > ; const EMPTY_ELEMENT_PLACEHOLDER = "EMPTY-ELEMENT-PLACEHOLDER" ; export async function convertBemToBlocks ( files ) { for ( let file of files ) { let fileName = path . relative ( process . cwd ( ) , file ) ; let css = fs . readFileSync ( file , "utf-8" ) ; let result = await processBEMContents ( fileName , css ) ; const parsedFilePath = path . parse ( file ) ; const blockFilePath = Object . assign ( parsedFilePath , { ext : `.block${ parsedFilePath . ext }` , base : undefined } ) ; await fs . writeFile ( path . format ( blockFilePath ) , result ) ; } } export async function processBEMContents ( fileName , css ) { let processor = postcss ( [ vars ( { variables : ( ) => { return { fileName } ; } , } ) , bemToBlocksPlugin , ] ) ; let result = await processor . process ( css , { from : fileName } ) ; return result . toString ( ) ; } param bemSelectorCache export function constructBlocksMap ( bemSelectorCache ) { let blockListMap = new Map ( ) ; let resultMap = new WeakMap ( ) ; for ( let bemSelector of bemSelectorCache . values ( ) ) { let blockClass = new BlockClassSelector ( ) ; if ( bemSelector . element ) { blockClass . class = bemSelector . element ; } if ( bemSelector . modifier ) { blockClass . state = bemSelector . modifier ; if ( blockClass . state . match ( /^is-/ ) ) { blockClass . state = blockClass . state . substring ( 3 ) ; } } resultMap . set ( bemSelector , blockClass ) ; let block = blockListMap . get ( bemSelector . block ) ; if ( block ) { if ( bemSelector . element ) { if ( block . has ( bemSelector . element ) ) { ( block . get ( bemSelector . element ) as BemSelector [ ] ) . push ( bemSelector ) ; } else { block . set ( bemSelector . element , new Array ( bemSelector ) ) ; } } else { if ( block . has ( EMPTY_ELEMENT_PLACEHOLDER ) ) { ( block . get ( EMPTY_ELEMENT_PLACEHOLDER ) as BemSelector [ ] ) . push ( bemSelector ) ; } else { block . set ( EMPTY_ELEMENT_PLACEHOLDER , new Array ( bemSelector ) ) ; } } } else { let elementListMap = new Map ( ) ; if ( bemSelector . element ) { elementListMap . set ( bemSelector . element , new Array ( bemSelector ) ) ; } else { elementListMap . set ( EMPTY_ELEMENT_PLACEHOLDER , new Array ( bemSelector ) ) ; } blockListMap . set ( bemSelector . block , elementListMap ) ; } } for ( let elementListMap of blockListMap . values ( ) ) { for ( let selList of elementListMap . values ( ) ) { let lcsMap ; let modifiers = selList . length && selList . filter ( sel => sel . modifier !== undefined ) ; if ( modifiers ) { if ( modifiers . length > 1 ) { lcsMap = findLcsMap ( modifiers . map ( sel => sel . modifier as string ) ) ; modifiers . forEach ( sel => { let blockClass = resultMap . get ( sel ) ; let lcs = blockClass && blockClass . state && lcsMap [ blockClass . state ] ; if ( blockClass && blockClass . state && lcs ) { blockClass . subState = blockClass . state . replace ( `${ lcs }-` , "" ) ; blockClass . state = lcs . replace ( /-$/ , "" ) ; } } ) ; } } } } return resultMap ; } /**
 * PostCSS plugin for transforming BEM to CSS blocks
 */ export const bemToBlocksPlugin = postcss . plugin ( "bem-to-blocks-plugin" , ( options ) => { options = options || { } ; return async ( root , result ) => { let fileNameObject = result . messages . find ( varObj => { return varObj . name === "fileName" ; } ) ; let fileName = fileNameObject ? fileNameObject . value : undefined ; const bemSelectorCache = new Map ( ) ; const buildCache = async ( selectors ) => { let promises = [ ] ; selectors . walk ( ( selector ) => { if ( selectorParser . isClassName ( selector ) ) { try { let bemSelector = new BemSelector ( selector . value ) ; if ( bemSelector . block ) { bemSelectorCache . set ( selector . value , bemSelector ) ; } } catch ( e ) { promises . push ( getBemNamesFromUser ( selector . value , fileName ) ) ; } } } ) ; let answers = await Promise . all ( promises ) ; for ( let userBem of answers ) { let selector = userBem . selector ; bemSelectorCache . set ( selector , new BemSelector ( selector , userBem . bemObj ) ) ; } } ; const rewriteSelectors = ( selectors ) => { selectors . walk ( ( selector ) => { if ( selectorParser . isClassName ( selector ) ) { let bemSelector = bemSelectorCache . get ( selector . value ) ; let blockClassName = bemSelector && bemToBlockClassMap . get ( bemSelector ) ; if ( blockClassName ) { let stripSelector = false ; if ( blockClassName . class ) { selector . value = blockClassName . class ; } else { let newScopeNode = selectorParser . pseudo ( { value : ":scope" , } ) ; selector . parent ! . insertBefore ( selector , newScopeNode ) ; stripSelector = true ; } if ( blockClassName . state ) { let newAttrNode = selectorParser . attribute ( { attribute : blockClassName . state , quoteMark : blockClassName . subState ? '"' : undefined , operator : blockClassName . subState ? "=" : undefined , value : blockClassName . subState , raws : { value : blockClassName . subState ? `"${ blockClassName . subState }"` : undefined } , } ) ; selector . parent ! . insertAfter ( selector , newAttrNode ) ; } if ( stripSelector ) { selector . remove ( ) ; } } } } ) ; return selectors . toString ( ) ; } ; let bemToBlockClassMap ; let buildCachePromises = [ ] ; root . walkRules ( async ( rule ) => { buildCachePromises . push ( selectorParser ( buildCache ) . process ( rule ) ) ; } ) ; await Promise . all ( buildCachePromises ) ; bemToBlockClassMap = constructBlocksMap ( bemSelectorCache ) ; root . walkRules ( rule => { rule . selector = selectorParser ( rewriteSelectors ) . processSync ( rule . selector ) ; } ) ; result . root = root ; } ; } ) ;