tailwind-sorter 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e1f3de5d352e6a073f1767524d2c7b3ccc16940694a6b876e9a96d6cc0b680b
4
- data.tar.gz: e1210fc5d3bfac44ae7a077aa8801299b78a129c1688502b9621bb9a70ebaf0a
3
+ metadata.gz: a5b16162e06cb7a443fe14cb3789db5c9acc19ee0f142d6033645e52b3e54ac5
4
+ data.tar.gz: 2f8f31eebb9b06407e38dc0e955324c543a2f7a3bd5f0826474055322f1710d9
5
5
  SHA512:
6
- metadata.gz: a7bc6b3eb7efe05511fbb215831d7e904ccd2d2988e6b9e7672813f40515a98d35e066066cc2b3640da3b1f8b55130c9eb45600c2773a0286ef054a688ca8063
7
- data.tar.gz: 5448c1f0386877c77c50558cf1646cfa75e074a42369b47aa8a927a33dd493d18baf6446e4c7c8360fc235d6378677af9a7abf307bab18b496c16dc3f8205494
6
+ metadata.gz: 0ee3992e5ef6237c10a56d44955a84f4791bbdc021238c72de8cc8a4054076486a0da17eb6626f742a59922baf83758000a5dc3cf248984da568c6113acd60f3
7
+ data.tar.gz: 13ac69950d1fe543a14bf32752caa323a38fa23704c6e311517307bf506d17f8e55cea0226bca03b17796911737d031dfd8fe450c2414c8ce866493b665216cd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ### 0.5.0
2
+
3
+ Restructure the gem completely:
4
+
5
+ - extract `FileSorter` (that can sort classes in a file) and a new `StringSorter` (that can sort classes in a string)
6
+ - clean up the code into their own modules / classes
7
+ - add a DSL for running the sorters from ruby code
8
+ - enhance the tests
9
+
10
+ **BREAKING CHANGE**: if you called the file sorter from ruby like this:
11
+
12
+ ```ruby
13
+ TailwindSorter::Sorter.run(file_name, warn_only: true/false, config_file: "some_config")
14
+ ```
15
+
16
+ you need to change the call to the new syntax:
17
+
18
+ ```ruby
19
+ TailwindSorter.sort_file(file_name, warn_only: true/false, config_file: "some_config")
20
+
21
+ ```
22
+
1
23
  ### 0.4.1
2
24
 
3
25
  Add support for processing multiple files at once.
data/README.md CHANGED
@@ -2,12 +2,11 @@
2
2
 
3
3
  **A ruby gem to sort the [Tailwind CSS](https://tailwindcss.com) classes in your templates _the custom way_.**
4
4
 
5
- The gem contains a standalone executable script that can work in two ways:
5
+ The gem contains a sorting library and a standalone executable script that can work in two ways:
6
6
 
7
7
  - it can edit the given file in place (especially useful when hooked up to a file changes watcher) or
8
8
  - it can just generate warning messages suitable for [Overcommit](https://github.com/sds/overcommit),
9
- [Lefthook](https://github.com/evilmartians/lefthook) or any other similar system (or people, if that’s what you
10
- prefer).
9
+ [Lefthook](https://github.com/evilmartians/lefthook) or any other similar system.
11
10
 
12
11
  Out of the box the script supports sorting classes in [Slim templates](http://slim-lang.com/) but can be configured for
13
12
  anything else. The script also removes duplicate classes.
@@ -192,20 +191,36 @@ problems found.
192
191
  To run the sorter from ruby code, use the following line:
193
192
 
194
193
  ```ruby
195
- TailwindSorter::Sorter.run("app/views/my_template.html.slim")
194
+ TailwindSorter.sort_file("app/views/my_template.html.slim")
195
+ ```
196
+
197
+ You can also optionally pass in some arguments such as `warn_only: true` to only show warning instead of overwriting the file or `config_file: "path/to/my/config_file.yml"` for custom config path.
198
+
199
+ ## Sorting Tailwind classes from ruby code
200
+
201
+ You can also use this gem as a library and sort a single string with Tailwind classes:
202
+
203
+ ```ruby
204
+ >> TailwindSorter.sort("my-4 block absolute")
205
+ => "absolute block my-4"
206
+ ```
207
+
208
+ Optionally also with a custom config file:
209
+
210
+ ```ruby
211
+ TailwindSorter.sort("my-4 block absolute", config_file: "path/to/my/config_file.yml")
196
212
  ```
197
213
 
198
- You can also optionally pass in ome arguments such as `warn_only: true` to only show warning instead of overwriting the file or `config_file: "path/to/my/config_file.yml"` for custom config path.
199
214
 
200
215
  ## Running tests
201
216
 
202
217
  ```sh
203
218
  bundle install # to install the rspec gem
204
219
  bundle exec rspec
205
- .................
220
+ ..................................
206
221
 
207
- Finished in 1.08 seconds (files took 0.03424 seconds to load)
208
- 17 examples, 0 failures
222
+ Finished in 0.28774 seconds (files took 0.06508 seconds to load)
223
+ 34 examples, 0 failures
209
224
  ```
210
225
 
211
226
  ## Answers for the curious
@@ -215,3 +230,5 @@ Finished in 1.08 seconds (files took 0.03424 seconds to load)
215
230
  When we initially reordered CSS classes in all our templates (~900 Slim files) with the script changing nearly 4000
216
231
  lines, the whole process took less than 30 seconds. This makes the processing speed of approximately 30 files per
217
232
  second. Judge for yourself if this is fast enough for your needs or not.
233
+
234
+ **Update**: this is a benchmark of an old version of the gem which supported sorting only a single file at once. Since version 0.4.1 the script can process multiple files in a single run and as this skips repeated loading of ruby it speeds up the bulk sorting process in an order of magnitude.
data/exe/tailwind_sorter CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+ lib = File.expand_path("#{File.dirname(__FILE__)}/../lib")
4
4
  $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
5
 
6
6
  require "tailwind_sorter"
7
7
 
8
8
  warn_only = false
9
- config_file = "config/tailwind_sorter.yml"
9
+ config_file = TailwindSorter::Config::DEFAULT_CONFIG_FILE
10
10
 
11
11
  while (arg = ARGV.shift)
12
12
  case arg
@@ -22,9 +22,9 @@ end
22
22
 
23
23
  begin
24
24
  while (file_name = ARGV.shift)
25
- TailwindSorter::Sorter.run(file_name, warn_only: warn_only, config_file: config_file)
25
+ TailwindSorter.sort_file(file_name, warn_only:, config_file:)
26
26
  end
27
27
  rescue ArgumentError => e
28
- STDERR.puts e.message
28
+ warn e.message
29
29
  exit 1
30
30
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TailwindSorter
4
+ class Config
5
+ DEFAULT_CONFIG_FILE = "config/tailwind_sorter.yml"
6
+
7
+ def initialize(config_file = DEFAULT_CONFIG_FILE)
8
+ unless config_file && File.exist?(config_file)
9
+ raise ArgumentError, "Configuration file '#{config_file}' does not exist"
10
+ end
11
+
12
+ @config_file = config_file
13
+ end
14
+
15
+ def load
16
+ @config = YAML.load_file(@config_file)
17
+ convert_class_order_regexps!
18
+ @config
19
+ end
20
+
21
+ private
22
+
23
+ def convert_class_order_regexps!
24
+ @config["classes_order"].each_value do |class_patterns|
25
+ class_patterns.map! do |class_or_pattern|
26
+ if (patterns = class_or_pattern.match(%r{\A/(.*)/\z}).to_a).empty?
27
+ class_or_pattern
28
+ else
29
+ /\A#{patterns.last}\z/
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tailwind_sorter/config"
4
+ require "tailwind_sorter/string_sorter"
5
+ require "tailwind_sorter/file_sorter"
6
+
7
+ module TailwindSorter
8
+ module DSL
9
+ def sort(classes_string, config_file: Config::DEFAULT_CONFIG_FILE)
10
+ TailwindSorter::StringSorter.new(config_file:).sort(classes_string)
11
+ end
12
+
13
+ def sort_file(file, warn_only: false, config_file: Config::DEFAULT_CONFIG_FILE)
14
+ TailwindSorter::FileSorter.new(warn_only:, config_file:).sort(file)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require "yaml"
5
+
6
+ require "tailwind_sorter/config"
7
+ require "tailwind_sorter/sortable"
8
+
9
+ module TailwindSorter
10
+ class FileSorter
11
+ include Sortable
12
+
13
+ def initialize(warn_only: false, config_file: Config::DEFAULT_CONFIG_FILE)
14
+ @warn_only = warn_only
15
+ @config = Config.new(config_file).load
16
+
17
+ @sorting_keys_cache = {}
18
+ end
19
+
20
+ def sort(file_name)
21
+ raise ArgumentError, "File '#{file_name}' does not exist" unless file_name && File.exist?(file_name)
22
+
23
+ sort_classes(file_name)
24
+ end
25
+
26
+ private
27
+
28
+ def sort_classes(file)
29
+ warnings = []
30
+
31
+ infile = File.open(file)
32
+ outfile = Tempfile.create("#{File.basename(file)}.sorted")
33
+ changed = false
34
+
35
+ infile.each_line do |line|
36
+ line_number = infile.lineno
37
+
38
+ line_regexps_for(file).each_value do |regexp_config|
39
+ regexp = regexp_config["regexp"]
40
+ prefix = regexp_config["class_prefix"]
41
+ split_by = "#{regexp_config['class_splitter']}#{prefix}"
42
+
43
+ next unless (match = line.match(regexp))
44
+
45
+ classes = match["classes"]
46
+ classes = classes.split(split_by).map(&:strip).reject(&:empty?).uniq
47
+ sorted_classes = sort_classes_array(classes)
48
+
49
+ if @warn_only
50
+ orig_keys = classes.map { |css_class| sorting_key_lambda.call(css_class) }
51
+ sorted_keys = sorted_classes.map { |css_class| sorting_key_lambda.call(css_class) }
52
+
53
+ if orig_keys != sorted_keys
54
+ warning = "#{file}:#{line_number}:CSS classes are not sorted well. Please run 'tailwind_sorter #{file}'."
55
+ warnings << warning
56
+ puts(warning)
57
+ end
58
+ else
59
+ orig_line = line.dup unless changed
60
+ line.sub!(regexp, "\\k<before>#{prefix}#{sorted_classes.join(split_by)}")
61
+ changed = true if !changed && line != orig_line
62
+ end
63
+ end
64
+
65
+ outfile.puts line
66
+ end
67
+
68
+ success = true
69
+
70
+ warnings
71
+ rescue StandardError => e
72
+ warn "An error occurred: #{e}"
73
+ success = false
74
+ ensure
75
+ infile.close
76
+ outfile.close
77
+
78
+ success && changed ? FileUtils.mv(outfile, file) : File.unlink(outfile)
79
+ end
80
+
81
+ def line_regexps_for(file)
82
+ line_regexps = @config["regexps"].select { |_k, v| v["file_extension"] == File.extname(file) }
83
+ line_regexps.each_value { |v| v["regexp"] = Regexp.new(v["regexp"]) }
84
+ line_regexps
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TailwindSorter
4
+ module Sortable
5
+ private
6
+
7
+ # Returns the Tailwind classes array sorted according to the config file.
8
+ def sort_classes_array(classes)
9
+ classes = classes.map do |css_class_with_variants|
10
+ sort_variants(css_class_with_variants)
11
+ end
12
+
13
+ classes.sort_by { |css_class| sorting_key_lambda.call(css_class) }
14
+ end
15
+
16
+ # Lambda for sorting the Tailwind CSS classes. It is memoized to avoid repeated class groups lookups.
17
+ def sorting_key_lambda(default_index: 0)
18
+ @sorting_key_lambda ||= begin
19
+ class_groups = @config["classes_order"].keys
20
+ ->(tw_class) { sorting_key(tw_class, class_groups:, default_index:) }
21
+ end
22
+ end
23
+
24
+ # Reorders multiple variants, e.g.: "focus:sm:block" -> "sm:focus:block".
25
+ def sort_variants(css_class_with_variants)
26
+ *variants, css_class = css_class_with_variants.split(":")
27
+ return css_class_with_variants if variants.length <= 1
28
+
29
+ variants.sort_by! { |variant| @config["variants_order"].index(variant) }
30
+ "#{variants.join(':')}:#{css_class}"
31
+ end
32
+
33
+ # Constructs the sorting key for sorting CSS classes in the following way:
34
+ #
35
+ # group_index, variant1_index, variant2_index, class_index
36
+ # "sm:focus:flex" -> "01,01,11,0010"
37
+ # "flex" -> "01,00,00,0010"
38
+ # "custom-class" -> "00,00,00,0000"
39
+ def sorting_key(css_class_with_variants, class_groups:, default_index: 0)
40
+ return @sorting_keys_cache[css_class_with_variants] if @sorting_keys_cache[css_class_with_variants]
41
+
42
+ *variants, css_class = css_class_with_variants.split(":")
43
+
44
+ matching_index_in_group = nil
45
+ matching_group = class_groups.find do |group|
46
+ matching_index_in_group ||= @config["classes_order"][group].index { _1 === css_class }
47
+ end
48
+
49
+ key = [
50
+ format("%02d", (matching_group && class_groups.index(matching_group)) || default_index),
51
+ format("%02d", @config["variants_order"].index(variants[0]) || default_index),
52
+ format("%02d", @config["variants_order"].index(variants[1]) || default_index),
53
+ format("%04d", matching_index_in_group || default_index)
54
+ ].join(",")
55
+
56
+ @sorting_keys_cache[css_class_with_variants] = key
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ require "tailwind_sorter/config"
6
+ require "tailwind_sorter/sortable"
7
+
8
+ module TailwindSorter
9
+ class StringSorter
10
+ include Sortable
11
+
12
+ def initialize(config_file: Config::DEFAULT_CONFIG_FILE)
13
+ @config = Config.new(config_file).load
14
+ @sorting_keys_cache = {}
15
+ end
16
+
17
+ def sort(classes_string)
18
+ sort_classes_array(classes_string.split.map(&:strip).reject(&:empty?).uniq).join(" ")
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TailwindSorter
2
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
3
5
  end
@@ -1,5 +1,9 @@
1
- require "tailwind_sorter/sorter"
1
+ # frozen_string_literal: true
2
+
3
+ require "tailwind_sorter/config"
4
+ require "tailwind_sorter/dsl"
2
5
  require "tailwind_sorter/version"
3
6
 
4
7
  module TailwindSorter
8
+ extend TailwindSorter::DSL
5
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tailwind-sorter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - NejŘemeslníci.cz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-04 00:00:00.000000000 Z
11
+ date: 2024-08-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Simple but customizable sorter for TailwindCSS classes
14
14
  email:
@@ -23,7 +23,11 @@ files:
23
23
  - Rakefile
24
24
  - exe/tailwind_sorter
25
25
  - lib/tailwind_sorter.rb
26
- - lib/tailwind_sorter/sorter.rb
26
+ - lib/tailwind_sorter/config.rb
27
+ - lib/tailwind_sorter/dsl.rb
28
+ - lib/tailwind_sorter/file_sorter.rb
29
+ - lib/tailwind_sorter/sortable.rb
30
+ - lib/tailwind_sorter/string_sorter.rb
27
31
  - lib/tailwind_sorter/version.rb
28
32
  homepage: https://github.com/NejRemeslnici/tailwind-sorter
29
33
  licenses:
@@ -1,148 +0,0 @@
1
- require "tempfile"
2
- require "yaml"
3
-
4
- module TailwindSorter
5
- class Sorter
6
- def initialize
7
- @sorting_keys_cache = {}
8
- end
9
-
10
- # reorders multiple variants, e.g.: "focus:sm:block" -> "sm:focus:block"
11
- def sort_variants(css_class_with_variants, variants_order:)
12
- *variants, css_class = css_class_with_variants.split(":")
13
- return css_class_with_variants if variants.length <= 1
14
-
15
- variants.sort_by! { |variant| variants_order.index(variant) }
16
- "#{variants.join(':')}:#{css_class}"
17
- end
18
-
19
- # Constructs the sorting key for sorting CSS classes in the following way:
20
- #
21
- # group_index, variant1_index, variant2_index, class_index
22
- # "sm:focus:flex" -> "01,01,11,0010"
23
- # "flex" -> "01,00,00,0010"
24
- # "custom-class" -> "00,00,00,0000"
25
- def sorting_key(css_class_with_variants, variants_order:, classes_order:, class_groups:, default_index: 0)
26
- return @sorting_keys_cache[css_class_with_variants] if @sorting_keys_cache[css_class_with_variants]
27
-
28
- *variants, css_class = css_class_with_variants.split(":")
29
-
30
- matching_index_in_group = nil
31
- matching_group = class_groups.find do |group|
32
- matching_index_in_group ||= classes_order[group].index { _1 === css_class }
33
- end
34
-
35
- key = [
36
- format("%02d", matching_group && class_groups.index(matching_group) || default_index),
37
- format("%02d", variants_order.index(variants[0]) || default_index),
38
- format("%02d", variants_order.index(variants[1]) || default_index),
39
- format("%04d", matching_index_in_group || default_index)
40
- ].join(",")
41
-
42
- # puts "#{css_class_with_variants} #{key}"
43
- @sorting_keys_cache[css_class_with_variants] = key
44
- end
45
-
46
- def convert_regexps!(classes_order)
47
- classes_order.each do |group, class_patterns|
48
- class_patterns.map! do |class_or_pattern|
49
- if !(patterns = class_or_pattern.match(%r{\A/(.*)/\z}).to_a).empty?
50
- Regexp.new(/\A#{patterns.last}\z/)
51
- else
52
- class_or_pattern
53
- end
54
- end
55
- end
56
-
57
- classes_order
58
- end
59
-
60
- def sort_classes(file, regexps:, variants_order:, classes_order:, default_index: 0, warn_only: false)
61
- convert_regexps!(classes_order)
62
- class_groups = classes_order.keys
63
- warnings = []
64
-
65
- infile = File.open(file)
66
- outfile = Tempfile.create("#{File.basename(file)}.sorted")
67
-
68
- calculate_sorting_key = lambda do |css_class_with_variants|
69
- sorting_key(css_class_with_variants, variants_order:, classes_order:, class_groups:, default_index:)
70
- end
71
-
72
- changed = false
73
- infile.each_line do |line|
74
- line_number = infile.lineno
75
-
76
- regexps.each do |_regexp_name, regexp_config|
77
- regexp = regexp_config["regexp"]
78
- prefix = regexp_config["class_prefix"]
79
- split_by = "#{regexp_config['class_splitter']}#{prefix}"
80
-
81
- next unless (match = line.match(regexp))
82
-
83
- matched_classes = match["classes"]
84
- # puts "#{line_number}: #{matched_classes}"
85
-
86
- classes = matched_classes.split(split_by).map(&:strip).reject(&:empty?).uniq.map do |css_class_with_variants|
87
- sort_variants(css_class_with_variants, variants_order: variants_order)
88
- end
89
-
90
- sorted_classes = classes.sort_by { |css_class| calculate_sorting_key.call(css_class) }
91
- # puts sorted_classes.join('.')
92
-
93
- if warn_only
94
- orig_keys = classes.map { |css_class| calculate_sorting_key.call(css_class) }
95
- sorted_keys = sorted_classes.map { |css_class| calculate_sorting_key.call(css_class) }
96
- # puts orig_keys.inspect
97
- # puts sorted_keys.inspect
98
- if orig_keys != sorted_keys
99
- warning = "#{file}:#{line_number}:CSS classes are not sorted well. Please run 'tailwind_sorter #{file}'."
100
- warnings << warning
101
- puts(warning)
102
- end
103
- else
104
- orig_line = line.dup unless changed
105
- line.sub!(regexp, "\\k<before>#{prefix}#{sorted_classes.join(split_by)}")
106
- changed = true if !changed && line != orig_line
107
- end
108
- end
109
-
110
- outfile.puts line
111
- end
112
-
113
- success = true
114
-
115
- warnings
116
-
117
- rescue StandardError => e
118
- warn "An error occurred: #{e}"
119
- success = false
120
-
121
- ensure
122
- infile.close
123
- outfile.close
124
-
125
- success && changed ? FileUtils.mv(outfile, file) : File.unlink(outfile)
126
- end
127
-
128
- def run(file_name, warn_only: false, config_file: "config/tailwind_sorter.yml")
129
- raise ArgumentError, "File '#{file_name}' does not exist" unless file_name && File.exist?(file_name)
130
- raise ArgumentError, "Configuration file '#{config_file}' does not exist" unless config_file && File.exist?(config_file)
131
-
132
- config = YAML.load_file(config_file)
133
- file_extension = File.extname(file_name)
134
-
135
- regexps = config["regexps"].select { |_k, v| v["file_extension"] == file_extension }
136
- regexps.each { |_k, v| v["regexp"] = Regexp.new(v["regexp"]) }
137
-
138
- sort_classes(file_name, regexps:,
139
- classes_order: config["classes_order"],
140
- variants_order: config["variants_order"],
141
- warn_only:)
142
- end
143
-
144
- def self.run(...)
145
- new.run(...)
146
- end
147
- end
148
- end