Introduction
As a developer, I recently found myself faced with an exciting challenge: modernizing a legacy C# .NET codebase that was still using Bootstrap 3. The goal was clear - bring the project up to speed with the latest Bootstrap 5. However, I quickly realized that making such a significant leap could be risky and time-consuming.
That's when I decided to take a phased approach:
- First, migrate from Bootstrap 3 to Bootstrap 4
- Then, once stable, make the jump from Bootstrap 4 to Bootstrap 5
This strategy would allow for a more manageable transition, easier debugging, and a smoother overall process. Today, I'm excited to share the first part of this journey - automating the migration from Bootstrap 3 to 4 using a Python script.
A Note on the Code
Before we dive in, it's important to note that the code presented here is a simplified version of the actual script used in the project. For obvious reasons, such as proprietary information and specific project requirements, I've streamlined the code for this blog post. However, the approach and core functionality remain very similar to what was implemented in the real-world scenario.
The Challenge
Migrating from Bootstrap 3 to 4 involves numerous class name changes and deprecated components. Manually updating these across an entire project can be time-consuming and error-prone. That's where our Python script comes in.
The Solution
Our script, which we'll call bootstrap_migrator.py
, is designed to scan your project files and automatically update Bootstrap 3 class names to their Bootstrap 4 equivalents. It handles HTML, Razor (cshtml), and even JavaScript files, making it a comprehensive solution for your migration needs.
Breaking Down the Code
Let's dive into the details of our migration script and explain each part.
Importing Required Modules
import os
import re
We start by importing two essential Python modules:
-
os
: This module provides a way to use operating system dependent functionality, like navigating the file system. -
re
: This module provides support for regular expressions in Python.
The Main Migration Function
def update_bootstrap_classes(content, file_type):
class_mappings = {
r'\bcol-xs-(\d+)\b': r'col-\1',
r'\bcol-sm-(\d+)\b': r'col-sm-\1',
r'\bcol-md-(\d+)\b': r'col-md-\1',
r'\bcol-lg-(\d+)\b': r'col-lg-\1',
r'\bcol-xl-(\d+)\b': r'col-xl-\1',
r'\bbtn-default\b': 'btn-secondary',
r'\bimg-responsive\b': 'img-fluid',
r'\bimg-circle\b': 'rounded-circle',
r'\bimg-rounded\b': 'rounded',
r'\bpanel\b': 'card',
r'\bpanel-heading\b': 'card-header',
r'\bpanel-title\b': 'card-title',
r'\bpanel-body\b': 'card-body',
r'\bpanel-footer\b': 'card-footer',
r'\bpanel-primary\b': 'card bg-primary text-white',
r'\bpanel-success\b': 'card bg-success text-white',
r'\bpanel-info\b': 'card text-white bg-info',
r'\bpanel-warning\b': 'card bg-warning',
r'\bpanel-danger\b': 'card bg-danger text-white',
r'\bwell\b': 'card card-body',
r'\bthumbnail\b': 'card card-body',
r'\blist-inline\s*>\s*li\b': 'list-inline-item',
r'\bdropdown-menu\s*>\s*li\b': 'dropdown-item',
r'\bnav\s+navbar\s*>\s*li\b': 'nav-item',
r'\bnav\s+navbar\s*>\s*li\s*>\s*a\b': 'nav-link',
r'\bnavbar-right\b': 'ml-auto',
r'\bnavbar-btn\b': 'nav-item',
r'\bnavbar-fixed-top\b': 'fixed-top',
r'\bnav-stacked\b': 'flex-column',
r'\bhidden-xs\b': 'd-none',
r'\bhidden-sm\b': 'd-sm-none',
r'\bhidden-md\b': 'd-md-none',
r'\bhidden-lg\b': 'd-lg-none',
r'\bvisible-xs\b': 'd-block d-sm-none',
r'\bvisible-sm\b': 'd-none d-sm-block d-md-none',
r'\bvisible-md\b': 'd-none d-md-block d-lg-none',
r'\bvisible-lg\b': 'd-none d-lg-block d-xl-none',
r'\bpull-right\b': 'float-right',
r'\bpull-left\b': 'float-left',
r'\bcenter-block\b': 'mx-auto d-block',
r'\binput-lg\b': 'form-control-lg',
r'\binput-sm\b': 'form-control-sm',
r'\bcontrol-label\b': 'col-form-label',
r'\btable-condensed\b': 'table-sm',
r'\bpagination\s*>\s*li\b': 'page-item',
r'\bpagination\s*>\s*li\s*>\s*a\b': 'page-link',
r'\bitem\b': 'carousel-item',
r'\bhelp-block\b': 'form-text',
r'\blabel\b': 'badge',
r'\bbadge\b': 'badge badge-pill'
}
This function is the heart of our script. It takes two parameters:
-
content
: The content of the file we're updating. -
file_type
: The type of file we're dealing with (HTML, JS, etc.).
The class_mappings
dictionary is crucial. It maps Bootstrap 3 class patterns (as regex) to their Bootstrap 4 equivalents. For example, col-xs-*
becomes just col-*
in Bootstrap 4.
Replacing Classes in HTML and Razor Files
def replace_class(match):
classes = match.group(1).split()
updated_classes = []
for cls in classes:
replaced = False
for pattern, replacement in class_mappings.items():
if re.fullmatch(pattern, cls):
updated_cls = re.sub(pattern, replacement, cls)
updated_classes.append(updated_cls)
replaced = True
break
if not replaced:
updated_classes.append(cls)
return f'class="{" ".join(updated_classes)}"'
if file_type in ['cshtml', 'html']:
return re.sub(r'class="([^"]*)"', replace_class, content)
This part handles the replacement of classes in HTML and Razor files:
- It finds all
class
attributes in the HTML. - For each class found, it checks if it matches any of our Bootstrap 3 patterns.
- If a match is found, it replaces the class with its Bootstrap 4 equivalent.
- Classes that don't match any patterns are left unchanged.
Updating JavaScript Selectors
def replace_js_selectors(match):
full_match = match.group(0)
method = match.group(1)
selector = match.group(2)
classes = re.findall(r'\.[-\w]+', selector)
for i, cls in enumerate(classes):
cls = cls[1:]
for pattern, replacement in class_mappings.items():
if re.fullmatch(pattern, cls):
new_cls = re.sub(pattern, replacement, cls)
classes[i] = f'.{new_cls}'
break
updated_selector = selector
for old_cls, new_cls in zip(re.findall(r'\.[-\w]+', selector), classes):
updated_selector = updated_selector.replace(old_cls, new_cls)
return f"{method}('{updated_selector}')"
if file_type == 'js':
js_jquery_methods = [
'querySelector', 'querySelectorAll', 'getElementById', 'getElementsByClassName',
'$', 'jQuery', 'find', 'children', 'siblings', 'parent', 'closest', 'next', 'prev',
'addClass', 'removeClass', 'toggleClass', 'hasClass'
]
method_pattern = '|'.join(map(re.escape, js_jquery_methods))
content = re.sub(rf"({method_pattern})\s*\(\s*['\"]([^'\"]+)['\"]\s*\)", replace_js_selectors, content)
return content
This section handles updating class names in JavaScript files:
- It defines a list of common JavaScript and jQuery methods that might use class selectors.
- It then uses regex to find these method calls and updates the class names in their selectors.
- It also updates class names used in jQuery's
.css()
method calls.
Processing Individual Files
def process_file(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
file_type = file_path.split('.')[-1].lower()
updated_content = update_bootstrap_classes(content, file_type)
if content != updated_content:
with open(file_path, 'w', encoding='utf-8') as file:
file.write(updated_content)
print(f"Updated: {file_path}")
else:
print(f"No changes: {file_path}")
except Exception as e:
print(f"Error processing {file_path}: {str(e)}")
This function handles the processing of individual files:
- It reads the content of the file.
- Determines the file type based on its extension.
- Calls
update_bootstrap_classes
to update the content. - If changes were made, it writes the updated content back to the file.
- It also handles exceptions and provides feedback on the process.
The Main Function
def main():
project_dir = input("Enter the path to your project directory: ")
print(f"Scanning directory: {project_dir}")
if not os.path.exists(project_dir):
print(f"The directory {project_dir} does not exist.")
return
files_found = False
for root, dirs, files in os.walk(project_dir):
for file in files:
if file.endswith(('.cshtml', '.html', '.js')):
files_found = True
file_path = os.path.join(root, file)
print(f"Processing file: {file_path}")
process_file(file_path)
if not files_found:
print("No .cshtml, .html, or .js files found in the specified directory.")
if __name__ == "__main__":
main()
The main
function ties everything together:
- It prompts the user for the project directory.
- It then walks through the directory, finding all relevant files (.cshtml, .html, .js).
- For each file found, it calls
process_file
to update its content. - It provides feedback on the process, including if no relevant files were found.
Key Features
- Comprehensive Class Updates: From grid classes to component-specific classes, the script covers a wide range of Bootstrap changes.
- JavaScript Support: It updates class names in various JavaScript and jQuery selectors, ensuring your dynamic content doesn't break.
- Flexibility: The script can be easily extended to include more class mappings or file types.
- Non-Destructive: It only modifies files where changes are necessary, leaving others untouched.
Using the Script
To use the script, simply run it and provide the path to your project directory when prompted. It will then process all relevant files, updating them as necessary.
python bootstrap_migrator.py
Limitations and Considerations
While this script automates a significant portion of the migration process, it's important to note that it's not a complete solution. You should still:
- Thoroughly test your application after running the script.
- Be aware of Bootstrap 4's new components and features that may require manual implementation.
- Review your custom CSS and JavaScript that might interact with Bootstrap classes.
Conclusion
This script provides a powerful, automated way to handle a large part of the Bootstrap 3 to 4 migration process, saving developers significant time and reducing the chance of manual errors. It represents the first step in our journey to modernize our legacy C# .NET codebase. Once we've successfully migrated to Bootstrap 4 and ensured stability, we'll tackle the next phase: moving from Bootstrap 4 to 5.
Remember, while automation is incredibly helpful, it's not a substitute for understanding the changes between Bootstrap versions. Use this script as a powerful aid in your migration process, but always couple it with your expertise and thorough testing.
Happy migrating!