Skip to content

Instantly share code, notes, and snippets.

@borama
Last active August 25, 2022 23:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save borama/952edd2a0dc286a8b4591bb3ff19fd03 to your computer and use it in GitHub Desktop.
Save borama/952edd2a0dc286a8b4591bb3ff19fd03 to your computer and use it in GitHub Desktop.
A script to compare the classes in two CSS files. Useful for migrating utility CSS systems, such as Tachyons to Tailwind. Please find all the context here: https://dev.to/nejremeslnici/migrating-tachyons-to-tailwind-css-part-i-ich.
#!/bin/env ruby
# Compare classes between Tachyons & Tailwind
#
# Please read https://dev.to/nejremeslnici/migrating-tachyons-to-tailwind-css-part-i-ich for more context.
#
# To use this, do the following:
# Install css_parser gem (https://github.com/premailer/css_parser)
# - `gem install css_parser`
#
# Make this script executable: `chmod a+x compare_classes.rb`.
#
# Prepare the final CSS file from Tachyons:
# - add Tachyons to asset pipeline: add `//= link tachyons.css` to app/assets/config/manifest.js
# - run asset precompilation: `bundle exec rails assets:precompile`
# - the compiled css file is now in public/assets/tachyons*.css
# - purge unused classes (we ignore those in the migration):
# `npx purgecss -con "app/views/**/*" "app/helpers/**/*" "app/presenters/**/*" "app/javascript/**/*.js" -css public/assets/tachyons*.css -o /tmp/`
# - rename the final CSS: `mv /tmp/tachyons*.css /tmp/tachyons.css`
# - the final CSS is now in /tmp/tachyons.css
#
# Prepare the final CSS from Tailwind:
# - temporarily remove the `@import 'tailwindcss/base';` line from app/javascript/stylesheets/nere-tailwind.pcss
# (to remove base styles)
# - temporarily disable css purging in tailwind.config.js: `purge: { enable: false }`
# - show a page in dev environment (to recompile the CSS via Webpacker) or run the recompilation again manually
# - copy the compiled CSS to temp: `cp public/packs/css/application.css /tmp/tailwind.css`
# - the final CSS is now in /tmp/tailwind.css
#
# Now, call this script from the project root:
# `resources/tailwind/compare_classes.rb /tmp/tachyons.css /tmp/tailwind.css migrated_classes.txt
#
# It will output three sections:
# - identical classes = same name & declarations
# - renamed classes = different name but same declarations
# - colliding classes = same name but different declarations
# - missing classes = different name & declarations
#
# for each class, an approximate usage count in the project will be shown, too (uses "grep" command).
#
# Configuration:
#
# - TEMPLATE_DIRECTORIES - the directories of templates or other code to search utility classes for
# - TACHYONS_MQS_REGEX - all responsive variants (postfixes) that we can expect in the Tachyons CSS file
# - TACHYONS_MIGRATED_MQS_REGEX - the responsive variants that we actually are about to migrate
# (may be the same as TACHYONS_MQS_REGEX or a subset)
# - TAILWIND_MQS_REGEX - all responsive variants (prefixes) that we can expect in the Tailwind CSS file
require "css_parser"
include CssParser # rubocop:disable Style/MixinUsage
require "yaml"
if ARGV.length < 2
puts "Compares CSS classes between Tachyons and Tailwind"
puts "Usage: compare_classes.rb path/to/tachyons.css path/to/tailwind.css [migrated_classes.txt]"
exit 1
end
TEMPLATE_DIRECTORIES = %w[app/views app/helpers app/javascript/src app/presenters].freeze
TACHYONS_MQS_REGEX = /-(ns|s|m|l)$/.freeze
TACHYONS_MIGRATED_MQS_REGEX = /-(ns|l)$/.freeze
TAILWIND_MQS_REGEX = /^\.(sm|md|lg|xl|2xl|32xl):/.freeze
def usage_count(class_name)
command = "grep -RE '\\b#{class_name.delete('.')}\\b' #{TEMPLATE_DIRECTORIES.join(' ')} | wc -l"
`#{command}`.strip.to_i
end
def normalize_selector(selector)
selector.delete('\\') # remove escaping
end
def normalize_declarations(css_declarations)
declarations = css_declarations.gsub(/:\s+/, ": ").split(/\s*;\s*/).sort.map do |declaration|
declaration.gsub(/\b0px\b/, "0") # "normalize" 0px to 0
.gsub(/^-[a-z-]+:.*$/, "") # remove prefixed properties and variables
.delete('\\') # remove escaping
.strip
end
declarations.select { |d| !d.nil? && d != "" }.join("; ")
end
tachyons_parser = CssParser::Parser.new
tachyons_parser.load_string!(File.read(ARGV[0]))
tailwind_parser = CssParser::Parser.new
tailwind_parser.load_string!(File.read(ARGV[1]))
tachyons = {}
tachyons_parser.each_selector do |selector, declarations, _specificity|
selector = normalize_selector(selector)
next if selector.match?(/^[^.]/) # only utility classes
next if selector.match?(/[ ,:]/) # no complex selector
next if selector.match?(/\..*\./) # no complex selector
next if selector.match?(/^\.(fg|hfg|fsfg|bg|hbg|b|hb|s|hs)-/) # no colors
next if selector.match?(/^\.(border-underline-.*):/) # special excludes
# extract media query
variant = selector.match?(TACHYONS_MQS_REGEX)
next if variant && !selector.match?(TACHYONS_MIGRATED_MQS_REGEX)
selector = selector.sub(TACHYONS_MIGRATED_MQS_REGEX, "") if variant
declarations = normalize_declarations(declarations)
next if declarations.nil? || declarations == ""
if tachyons[declarations]
# unless variant
# puts "Warning: not redefining declaration #{declarations} " \
# "from #{tachyons[declarations]} to #{selector}! (Tachyons)"
# end
next
end
tachyons[declarations] = selector
end
tailwind = {}
tailwind_parser.each_selector do |selector, declarations, _specificity|
selector = normalize_selector(selector)
next if selector.match?(/^[^.]/) # only utility classes
next if selector.match?(/[ ,]/) # no complex selectors
next if selector.match?(/^\.(hover|focus|focus-within|:):/) # no complex selectors
next if selector.match?(TAILWIND_MQS_REGEX) # no media query variants
declarations = normalize_declarations(declarations)
next if declarations.nil? || declarations == ""
if tailwind[declarations]
# puts "Warning: not redefining declaration #{declarations} from #{tailwind[declarations]} to #{selector}! (Tailwind)"
next
end
tailwind[declarations] = selector
end
# identical classes
output = []
(tachyons.keys & tailwind.keys).each do |declaration|
next if tachyons[declaration] != tailwind[declaration]
output << " #{tachyons[declaration]} ⟶ #{tailwind[declaration]} (#{usage_count(tachyons[declaration])})"
end
puts "Identical classes (#{output.size}):"
puts output.sort
to_migrate_count = 0
# renamed classes
output = []
(tachyons.keys & tailwind.keys).each do |declaration|
next if tachyons[declaration] == tailwind[declaration]
output << " #{tachyons[declaration]} ⟶ #{tailwind[declaration]} (#{usage_count(tachyons[declaration])})"
to_migrate_count += 1
end
puts "Renamed classes (#{output.size}):"
puts output.sort
# collisions
tachyons_rev = tachyons.invert
tailwind_rev = tailwind.invert
output = []
(tachyons_rev.keys & tailwind_rev.keys).each do |class_name|
next if tachyons_rev[class_name] == tailwind_rev[class_name]
output << " #{class_name} (#{usage_count(class_name)},
tachyons: '#{tachyons_rev[class_name]}',
tailwind: '#{tailwind_rev[class_name]}')".gsub(/[[:space:]]+/, " ").strip
to_migrate_count += 1
end
puts "Colliding classes (#{output.size}):"
puts output.sort
# missed classes
output = []
(tachyons.keys - tailwind.keys).each do |declaration|
output << " #{tachyons[declaration]} (#{usage_count(tachyons[declaration])})"
to_migrate_count += 1
end
puts "Missed classes (non-colours) (#{output.size}):"
puts output.sort
puts "Found #{to_migrate_count} classes still to migrate."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment