Created
August 1, 2013 03:15
-
-
Save macanderson/6128137 to your computer and use it in GitHub Desktop.
Python script to prefix all class and javascript files for bootstrap (version 3 supported)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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