From 4516d90ede4e845bc6e50f2e7ce34cb7ce6da782 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 25 Jan 2024 16:55:49 +1100 Subject: [PATCH] Add script to upgrade HAML syntax [skip-ci] --- lib/haml_up.rb | 110 +++++++++++++++++++++++++++++++++++++++ script/haml-up.rb | 22 ++++++++ spec/lib/haml_up_spec.rb | 50 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 lib/haml_up.rb create mode 100755 script/haml-up.rb create mode 100644 spec/lib/haml_up_spec.rb diff --git a/lib/haml_up.rb b/lib/haml_up.rb new file mode 100644 index 0000000000..d4eb20fb76 --- /dev/null +++ b/lib/haml_up.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +# Upgrade HAML attribute syntax to prepare for HAML 6. +# +# HAML 6 stopped supporting nested hash attributes other than `data` and `aria`. +# We used to be able to write: +# +# %div{ ng: { class: "upper", bind: "model" } } +# +# This needs to be written in a flat structure now: +# +# %div{ "ng-class" => "upper", "ng-bind" => "model" } +# +require "fileutils" +require "haml" + +class HamlUp + def upgrade_file(filename) + template = File.read(filename) + rewrite_template(template) + File.write(filename, template) + end + + def rewrite_template(template) + haml_attributes(template).compact.each do |attributes| + rewrite_attributes(template, attributes) + end + end + + def rewrite_attributes(template, original) + attributes = parse_attributes(original) + + if attributes.nil? # parser failed + puts "Warning: failed to parse:\n" # rubocop:disable Rails/Output + puts original # rubocop:disable Rails/Output + return + end + + parse_deprecated_hashes(attributes) + + to_transform = attributes.select { |_k, v| v.is_a? Hash } + + return if to_transform.empty? + + to_transform.each do |key, hash| + add_full_keys(attributes, key, hash) + attributes.delete(key) + end + + replace_attributes(template, original, attributes) + end + + def haml_attributes(template) + options = Haml::Options.new + parsed_tree = Haml::Parser.new(options).call(template) + elements = flatten_tree(parsed_tree) + elements.map { |e| e.value[:dynamic_attributes]&.old } + end + + def flatten_tree(parent) + parent.children.map do |child| + [child] + flatten_tree(child) + end.flatten + end + + def parse_attributes(string) + Haml::AttributeParser.parse(string) + end + + def parse_deprecated_hashes(hash) + hash.each do |key, value| + next if ["aria", "data"].include?(key) + + parsed = parse_attributes(value) + next unless parsed.is_a? Hash + + parse_deprecated_hashes(parsed) + hash[key] = parsed + end + end + + def add_full_keys(attributes, key, hash) + hash.each do |subkey, value| + full_key = "#{key}-#{subkey}" + if value.is_a? Hash + add_full_keys(attributes, full_key, value) + else + attributes[full_key] = value + end + end + end + + def replace_attributes(template, original, attributes) + parsed_lines = original.split("\n") + lines_as_regex = parsed_lines.map(&Regexp.method(:escape)) + pattern = lines_as_regex.join("\n\s*") + + template.gsub!(/#{pattern}/, stringify(attributes)) + end + + def stringify(hash) + entries = hash.map do |key, value| + value = stringify(value) if value.is_a? Hash + + "#{key.inspect} => #{value}" + end + + "{ #{entries.join(', ')} }" + end +end diff --git a/script/haml-up.rb b/script/haml-up.rb new file mode 100755 index 0000000000..033e09f348 --- /dev/null +++ b/script/haml-up.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Upgrade HAML attribute syntax to prepare for HAML 6. +# +# HAML 6 stopped supporting nested hash attributes other than `data` and `aria`. +# We used to be able to write: +# +# %div{ ng: { class: "upper", bind: "model" } } +# +# This needs to be written in a flat structure now: +# +# %div{ "ng-class" => "upper", "ng-bind" => "model" } +# +# This script rewrites HAML files automatically. It may be used like: +# +# git ls-files '*.haml' | while read f; do ./haml-up.rb "$f"; done +# +require "haml_up" + +puts ARGV[0] +HamlUp.new.upgrade_file(ARGV[0]) diff --git a/spec/lib/haml_up_spec.rb b/spec/lib/haml_up_spec.rb new file mode 100644 index 0000000000..c95b8cc8d5 --- /dev/null +++ b/spec/lib/haml_up_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'haml_up' + +describe HamlUp do + describe "#rewrite_template" do + it "preserves a simple template" do + original = "%p This is a paragraph" + template = call(original) + expect(template).to eq original + end + + it "rewrites non-standard attribute hashes" do + original = "%p{ng: {click: 'action', show: 'condition'}} label" + template = call(original) + expect(template).to eq "%p{ \"ng-click\" => 'action', \"ng-show\" => 'condition' } label" + end + + it "preserves standard attribute hashes" do + original = "%p{data: {click: 'action', show: 'condition'}} label" + template = call(original) + expect(template).to eq original + end + + it "preserves standard attribute hashes while rewriting others" do + original = "%p{data: {click: 'standard'}, ng: {click: 'not'}} label" + template = call(original) + expect(template).to eq "%p{ \"data\" => {click: 'standard'}, \"ng-click\" => 'not' } label" + end + + it "rewrites multi-line attributes" do + original = <<~HAML + %li{ ng: { class: "{active: selector.active}" } } + %a{ "tooltip" => "{{selector.object.value}}", "tooltip-placement" => "bottom", + ng: { transclude: true, class: "{active: selector.active, 'has-tip': selector.object.value}" } } + HAML + expected = <<~HAML + %li{ "ng-class" => "{active: selector.active}" } + %a{ "tooltip" => "{{selector.object.value}}", "tooltip-placement" => "bottom", "ng-transclude" => true, "ng-class" => "{active: selector.active, 'has-tip': selector.object.value}" } + HAML + template = call(original) + expect(template).to eq expected + end + + def call(original) + original.dup.tap { |t| subject.rewrite_template(t) } + end + end +end