-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathbootstrap_namespace_prefixer.py
executable file
·165 lines (155 loc) · 8.96 KB
/
bootstrap_namespace_prefixer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env python
""" Adds a "tb-" (configurable) prefix to all Twitter Bootstrap CSS code
@author Francois Aucamp <francois.aucamp@gmail.com>
@license GPLv2+
"""
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 = 'tb-'
# 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', 'bootstrap-responsive.css', 'bootstrap-responsive.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')