wir
8/1/2013 - 3:15 AM

Python script to prefix all class and javascript files for bootstrap (version 3 supported)

Python script to prefix all class and javascript files for bootstrap (version 3 supported)

#!/usr/bin/env python

from __future__ import print_function
import sys, re
import os.path

""" The CSS classname/namespace prefix to prepend to all Bootstrap CSS classes """
CSS_CLASS_PREFIX = 'ns-'

# Not all CSS classes that are referenced in JavaScript are actually defined in the CSS script.
# This list allows the JavaScript prefix algorithm to recognize these "extra" classes
ADDITIONAL_CSS_CLASSES_IN_JS = ['collapsed']

# Note: regex uses semi-Python-specific \n (newline) character
#CSS_CLASS_REGEX = re.compile(r'\.([a-zA-Z][a-zA-Z0-9-_]+\w*)(?=[^\{,\n\}\(]*[\{,])') # e.g: .classname {
CSS_CLASS_REGEX = re.compile(r'(?<!progid:DXImageTransform)(?<!progid:DXImageTransform.Microsoft)(?!\.png)\.([a-zA-Z][a-zA-Z0-9-_]+\w*)(?=[^\{,\n]*[\{,])') # e.g: .classname {
CSS_CLASS_ATTRIBUTE_SELECTOR_REGEX = re.compile(r'(\[\s*class\s*[~|*^]?=\s*"\s*)([a-zA-Z][a-zA-Z0-9-_]+\w*)(")(?=[^\{,\n\}]*[\{,])') # e.g: [class~="someclass-"] 
JS_CSS_CLASS_REGEX_TEMPLATE = r"""(?<!(.\.on|\.off))(\(['"][^'"]*\.)(%s)([^'"]*['"]\))"""
JS_JQUERY_REGEX_TEMPLATE = r"""((addClass|removeClass|hasClass|toggleClass)\(['"])(%s)(['"])"""
JS_JQUERY_REGEX_TEMPLATE_VAR = r"""((addClass|removeClass|hasClass|toggleClass)\()([a-zA-Z0-9]+)(\))"""
JS_JQUERY_REGEX_TEMPLATE_LIST = r"""((addClass|removeClass|hasClass|toggleClass)\(\[)([^\]]*)(\])"""
JS_JQUERY_REGEX_STRING_MULTIPLE = re.compile(r"""((addClass|removeClass|hasClass|toggleClass)\(['"]\s*)(([^'"\s]+\s+)+[^'"]+)(\s*['"]\))""") # e.g: removeClass('fade in top bottom')
# Regex for the conditional/more tricky add/remove/hasClass calls in the bootstrap.js source
JS_JQUERY_CONDITIONAL_REGEX_TEMPLATE = r"""((addClass|removeClass|hasClass|toggleClass)'\]\(['"])(%s)(['"])""" # e.g: ? 'addClass' : 'removeClass']('someclass')
JS_JQUERY_INLINE_IF_CONDITION_REGEX_TEMPLATE = r"""(\)\s*\?\s*['"])(%s)(['"]\s*:\s*['"]{2})"""
# Regex for certain jquery selectors that might have been missed by the previous regexes
JS_JQUERY_SELECTOR_REGEX_TEMPLATE = r"""(:not\(\.)(%s)(\))"""
JS_INLINE_HTML_REGEX_TEMPLATE = r"""(class="[^"]*)(?<=\s|")(%s)(?=\s|")"""
# Some edge cases aren't easy to fix using generic regexes because of potential clashes with non-CSS code
JS_EDGE_CASE_1_TEMPLATE = r"""(this\.\$element\[\s*\w+\s*\]\(['"]\s*)(%s)(['"]\s*\))"""

def processCss(cssFilename):
    """ Adds the CSS_CLASS_PREFIX to each CSS class in the specified CSS file """
    print('Processing CSS file:', cssFilename)
    try:
        f = open(cssFilename)        
    except IOError:
        print(' Failed to open file; skipping:', cssFilename)
    else:
        css = f.read()
        f.close()        
        processedFilename = cssFilename[:-4] + '.prefixed.css'
        f = open(processedFilename, 'w')        
        processedCss = CSS_CLASS_REGEX.sub(r'.%s\1' % CSS_CLASS_PREFIX, css)
        processedCss = CSS_CLASS_ATTRIBUTE_SELECTOR_REGEX.sub(r'\1%s\2\3' % CSS_CLASS_PREFIX, processedCss)
        f.write(processedCss)
        f.close();
        print(' Prefixed CSS file written as:', processedFilename)

def collectCssClassnames(cssFilename):
    """ Returns a set of all the CSS class names in the specified CSS file """
    print('Collecting CSS classnames from file:', cssFilename)
    try:
        f = open(cssFilename)        
    except IOError:
        print(' Failed to open file; skipping:', cssFilename)
    else:
        css = f.read()
        f.close()
        classes = set(CSS_CLASS_REGEX.findall(css))
        # The "popover-inner" class is referred to in javascript, but not the CSS files - force prefixing for consistency
        classes.add('popover-inner')
        return classes

def processJs(jsFilename, cssClassNames):
    """ Adds the CSS_CLASS_PREFIX to each CSS class in the specified JavaScript file.
    Requires a list of CSS classes (to avoid confusion between custom events and CSS classes, etc)
    """
    print("Processing JavaScript file:", jsFilename)
    try:
        f = open(jsFilename)        
    except IOError:
        print(' Failed to open file; skipping:', jsFilename)
    else:
        regexClassNamesAlternatives =  '|'.join(cssClassNames)
        js = f.read()
        f.close()        
        jsCssClassRegex = re.compile(JS_CSS_CLASS_REGEX_TEMPLATE % regexClassNamesAlternatives)
        # Replace CSS classes iteratively to ensure all classes are modified (my regex isn't clever enough to do this in one pass only)
        modJs = jsCssClassRegex.sub(r'\2%s\3\4' % CSS_CLASS_PREFIX, js)
        while modJs != js:
            js = modJs
            modJs = jsCssClassRegex.sub(r'\2%s\3\4' % CSS_CLASS_PREFIX, js)            
        js = modJs
        del modJs
        # JQuery has/add/removeClass calls
        jqueryCssClassRegex = re.compile(JS_JQUERY_REGEX_TEMPLATE % regexClassNamesAlternatives)
        js = jqueryCssClassRegex.sub(r'\1%s\3\4' % CSS_CLASS_PREFIX, js)
        jqueryCssClassRegex = re.compile(JS_JQUERY_REGEX_TEMPLATE_VAR)
        js = jqueryCssClassRegex.sub(r"\1'%s'+\3\4" % CSS_CLASS_PREFIX, js)
        # List/array of variables or string literals
        jqueryCssClassRegex = re.compile(JS_JQUERY_REGEX_TEMPLATE_LIST)
        match = jqueryCssClassRegex.search(js)
        while match:
            listStr = match.group(3)
            items = listStr.split(',')
            processed = []
            for rawItem in items:
                item = rawItem.strip()
                if item[0] in ("'", '"'): # string literal
                    item = item[0] + CSS_CLASS_PREFIX + item[1:]
                else: # variable
                    item = "'%s'+%s" % (CSS_CLASS_PREFIX, item)
                processed.append(item)
            newList = ','.join(processed)
            js = js[0:match.start(3)] + newList + js[match.end(3):]
            match = jqueryCssClassRegex.search(js, match.start(3)+len(newList))
        # Modify multiple CSS classes that are referenced in a single string
        match = JS_JQUERY_REGEX_STRING_MULTIPLE.search(js)
        while match:
            newList = ' '.join(['%s%s' % (CSS_CLASS_PREFIX, item) if item in cssClassNames else item for item in match.group(3).split(' ')])
            js = js = js[0:match.start(3)] + newList + js[match.end(3):]
            match = JS_JQUERY_REGEX_STRING_MULTIPLE.search(js, match.start(3)+len(newList))
        # In-line conditional JQuery has/add/removeClass calls
        jqueryCssClassRegex = re.compile(JS_JQUERY_CONDITIONAL_REGEX_TEMPLATE % regexClassNamesAlternatives)
        js = jqueryCssClassRegex.sub(r'\1%s\3\4' % CSS_CLASS_PREFIX, js)
        # In-line if conditional structures
        jqueryCssClassRegex = re.compile(JS_JQUERY_INLINE_IF_CONDITION_REGEX_TEMPLATE % regexClassNamesAlternatives)        
        js = jqueryCssClassRegex.sub(r'\1%s\2\3' % CSS_CLASS_PREFIX, js)
        # Some sepcific jquery selectors that might have been missed
        jqueryCssClassRegex = re.compile(JS_JQUERY_SELECTOR_REGEX_TEMPLATE % regexClassNamesAlternatives)
        js = jqueryCssClassRegex.sub(r'\1%s\2\3' % CSS_CLASS_PREFIX, js)
        jqueryCssClassRegex = re.compile(JS_INLINE_HTML_REGEX_TEMPLATE % regexClassNamesAlternatives)
        # Replace inline-HTML CSS classes iteratively to ensure all classes are modified (my regex isn't clever enough to do this in one pass only)
        modJs = jqueryCssClassRegex.sub(r'\1%s\2' % CSS_CLASS_PREFIX, js)
        while modJs != js:
            js = modJs
            modJs = jqueryCssClassRegex.sub(r'\1%s\2' % CSS_CLASS_PREFIX, js)
        js = modJs
        del modJs
        # Finally, process some edge cases/exceptions which cannot be easily handled by more generic regexes
        jsEdgeCase1Regex = re.compile(JS_EDGE_CASE_1_TEMPLATE % regexClassNamesAlternatives)
        js = jsEdgeCase1Regex.sub(r'\1%s\2\3' % CSS_CLASS_PREFIX, js)
        # Write the output file
        processedFilename = jsFilename[:-3] + '.prefixed.js'    
        f = open(processedFilename, 'w')
        f.write(js)
        f.close();
        print(' Prefixed JavaScript file written as:', processedFilename)


if __name__ == '__main__':
    if len(sys.argv) < 2:
        print('Usage: %s <bootstrap_top_dir>' % sys.argv[0])
        sys.exit(1)
    else:
        bsTopDir = sys.argv[1]
        cssClassNames = None
        for cssFile in ('bootstrap.css', 'bootstrap.min.css'):
            cssFilePath = os.path.normpath(os.path.join(bsTopDir, 'css', cssFile))
            processCss(cssFilePath)
            if cssClassNames == None:
                cssClassNames = collectCssClassnames(cssFilePath)
                
        if cssClassNames != None:
            cssClassNames.update(ADDITIONAL_CSS_CLASSES_IN_JS)
            for jsFile in ('bootstrap.js', 'bootstrap.min.js'):
                jsFilePath = os.path.normpath(os.path.join(bsTopDir, 'js', jsFile))
                processJs(jsFilePath, cssClassNames)
        else:
            print('Failed to collect CSS class names - cannot modify JavaScript source files as a result')