Skip to content

Instantly share code, notes, and snippets.

@mikker
Last active January 3, 2024 07:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikker/3e16a704d24bcf7839fe4582d297be6f to your computer and use it in GitHub Desktop.
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.
#!/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