Last active
January 3, 2024 07:45
-
-
Save mikker/3e16a704d24bcf7839fe4582d297be6f to your computer and use it in GitHub Desktop.
Script to normalize Slim template class names into their attribute form. This more regular form works better with Tailwind, both the parser and the sorting formatters.
This file contains hidden or 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 ruby | |
class SlimLine | |
KNOWN_TAGS = "div|header|footer|aside|section|article|nav|main|ul|ol|li|a|p|h1|h2|h3|h4|h5|h6|span|button|form|input|textarea|select|option|label|fieldset|legend|table|thead|tbody|tr|td|th" | |
RE = %r{^(?<whitespace>\s*)(?<tag>(#{KNOWN_TAGS})\b)?(?<id>#[a-z\-]+)?(?<class_list>(\.[-a-z][a-z0-9\-:\/]*)+)?(?<rest>.*)?} | |
def initialize(line) | |
@line = line.dup | |
end | |
attr_reader :line | |
def convert | |
line.rstrip! | |
return line unless match = RE.match(@line) | |
return line if !match[:tag] && !match[:id] && !match[:class_list] | |
return line if match[:rest]&.start_with?(":") | |
tag = match[:tag] || "div" | |
return line unless KNOWN_TAGS.include?(tag) | |
str = match[:whitespace] || "" | |
str << tag | |
if match[:class_list] | |
classes = match[:class_list] | |
.split(".") | |
.map(&:strip) | |
.reject(&:empty?) | |
else | |
classes = [] | |
end | |
rest = (match[:rest] || "").strip | |
if rest.match?(/^\[[^\]]*$/) | |
# starting [ but no ending ] | |
str << "[" | |
rest.gsub!(/^\[(.*)/, "\\1") | |
else | |
str << " " | |
rest.gsub!(/^(\[(.*)\])/, "\\2") | |
end | |
# attributes with = | |
re = /([a-z0-9@\-\.:]+)\s?=\s?["']([^'"]*)["']/ | |
attrs = rest.scan(re).each_with_object({}) do |(k, v), h| | |
h[k] = v | |
end | |
rest.gsub!(re, "") | |
# attributes without = | |
re = /(data-|x-)[:a-z0-9\-]+/ | |
attrs.merge!( | |
rest.scan(re).each_with_object(attrs) do |k, h| | |
h[$&] = "" | |
end | |
) | |
rest.gsub!(re, "") | |
attrs["id"] = match[:id].gsub("#", "") if match[:id] | |
if classes.any? | |
attrs["class"] = classes.concat((attrs["class"] || "").split(" ")).uniq.join(" ") | |
end | |
if attrs.any? | |
str << attrs.sort.map { |k, v| "#{k}=\"#{v}\"" }.join(" ") | |
str.rstrip! | |
end | |
str.rstrip! | |
str << " " + rest.strip unless rest.empty? | |
str.rstrip | |
end | |
end | |
class SlimFile | |
def initialize(path) | |
@path = path | |
end | |
def convert | |
File | |
.readlines(@path) | |
.map do |line| | |
SlimLine | |
.new(line) | |
.convert | |
end | |
.join("\n") | |
end | |
end | |
def main | |
paths = Dir.glob("app/**/*.html.slim") | |
paths.each do |path| | |
slim = SlimFile.new(path) | |
converted = slim.convert | |
File.write(path, converted) | |
end | |
end | |
if defined?(RSpec) | |
RSpec.describe SlimLine do | |
def self.ex(line, expected) | |
it(line.inspect + " => " + expected.inspect) { expect(SlimLine.new(line).convert).to(eq(expected)) } | |
end | |
ex(".foo.bar", "div class=\"foo bar\"") | |
ex(".-m-1.bar", "div class=\"-m-1 bar\"") | |
ex(".p-1.border-border", "div class=\"p-1 border-border\"") | |
ex(".grid.lg:grid-cols-2", "div class=\"grid lg:grid-cols-2\"") | |
ex(".lg:pr-16.lg:w-1/4", "div class=\"lg:pr-16 lg:w-1/4\"") | |
ex(".foo.bar Content", "div class=\"foo bar\" Content") | |
ex(".foo.bar= variable", "div class=\"foo bar\" = variable") | |
ex("header.foo.bar", "header class=\"foo bar\"") | |
ex("header.foo.bar[data-thing]", "header class=\"foo bar\" data-thing=\"\"") | |
ex("header.foo.bar[data-thing=\"1 2 long\"]", "header class=\"foo bar\" data-thing=\"1 2 long\"") | |
ex(" footer.foo.bar[data-thing=\"1 2 long\"]", " footer class=\"foo bar\" data-thing=\"1 2 long\"") | |
ex(".foo[data-controller=\"thing\"", "div[class=\"foo\" data-controller=\"thing\"") | |
ex("input.ph0[", "input[class=\"ph0\"") | |
ex("#foo", "div id=\"foo\"") | |
ex("#foo.bar", "div class=\"bar\" id=\"foo\"") | |
ex(".foo[class=\"thing\"]", "div class=\"foo thing\"") | |
ex("div.hi[class=\"foo-[666]\"]", "div class=\"hi foo-[666]\"") | |
ex(".foo[class=\"foo\"]", "div class=\"foo\"") | |
ex("- if true", "- if true") | |
ex("= @title", "= @title") | |
ex("", "") | |
ex("div= @title", "div = @title") | |
ex("pattern=\"abc\"", "pattern=\"abc\"") | |
ex("button.btn[@click.prevent = \"fn()\"]= var", "button @click.prevent=\"fn()\" class=\"btn\" = var") | |
ex("h1 class=\"bg-red-500\" Admin", "h1 class=\"bg-red-500\" Admin") | |
ex("button.btn[@click.prevent = \"fn()\"]= t('.omg')", "button @click.prevent=\"fn()\" class=\"btn\" = t('.omg')") | |
ex(" label: t(\"omg\")", " label: t(\"omg\")") | |
ex("#top-nav.relative[data-turbo-permanent]", "div class=\"relative\" data-turbo-permanent=\"\" id=\"top-nav\"") | |
end | |
end | |
if __FILE__ == $PROGRAM_NAME | |
main | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment