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') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment