tailwind-sorter 0.4.0 → 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: 82b25d2e48c760db25e242cafa14a986831e6ccaff1498ea7fbf638dc22392d9
4
- data.tar.gz: dc94b034b7e7922bafc9f618a7db6f839d102eb2c38f916689f538fcfab07d2c
3
+ metadata.gz: a5b16162e06cb7a443fe14cb3789db5c9acc19ee0f142d6033645e52b3e54ac5
4
+ data.tar.gz: 2f8f31eebb9b06407e38dc0e955324c543a2f7a3bd5f0826474055322f1710d9
5
5
  SHA512:
6
- metadata.gz: 95a45a46c6a5911db7bd2e48fde99b7a752f2de25bc5832b009c0fe5d840912474be6047caf2f7ecb3d2325e894c466bab4ff9f32f71fe88506448ff204bb74c
7
- data.tar.gz: 98a11cdfd1bebff6fd7a8a24069326dc26776dffdcb4b37f8271374793c04133823ebd2c3375c5a6192cec5f5f68ac0b61e9322be9757658b102d32898f02358
6
+ metadata.gz: 0ee3992e5ef6237c10a56d44955a84f4791bbdc021238c72de8cc8a4054076486a0da17eb6626f742a59922baf83758000a5dc3cf248984da568c6113acd60f3
7
+ data.tar.gz: 13ac69950d1fe543a14bf32752caa323a38fa23704c6e311517307bf506d17f8e55cea0226bca03b17796911737d031dfd8fe450c2414c8ce866493b665216cd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
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
+
23
+ ### 0.4.1
24
+
25
+ Add support for processing multiple files at once.
26
+
1
27
  ### 0.4.0
2
28
 
3
29
  - Add support for regular expressions in configuration for class ordering (#3)
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.
@@ -163,6 +162,12 @@ The script requires the configuration file to be present in `config/tailwind_sor
163
162
  bin/tailwind_sorter -c path/to/my/config_file.yml app/views/my_template.html.slim
164
163
  ```
165
164
 
165
+ Multiple files can be processed in a single run:
166
+
167
+ ```sh
168
+ bin/tailwind_sorter app/views/my_template.html.slim app/views/another_template.html.slim
169
+ ```
170
+
166
171
  ## Running automatically via your IDE / editor
167
172
 
168
173
  Perhaps the best way to run the script is using your editor or IDE. Many editors provide the possibility to watch your
@@ -186,20 +191,36 @@ problems found.
186
191
  To run the sorter from ruby code, use the following line:
187
192
 
188
193
  ```ruby
189
- 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")
190
212
  ```
191
213
 
192
- 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.
193
214
 
194
215
  ## Running tests
195
216
 
196
217
  ```sh
197
218
  bundle install # to install the rspec gem
198
219
  bundle exec rspec
199
- .................
220
+ ..................................
200
221
 
201
- Finished in 1.08 seconds (files took 0.03424 seconds to load)
202
- 17 examples, 0 failures
222
+ Finished in 0.28774 seconds (files took 0.06508 seconds to load)
223
+ 34 examples, 0 failures
203
224
  ```
204
225
 
205
226
  ## Answers for the curious
@@ -209,3 +230,5 @@ Finished in 1.08 seconds (files took 0.03424 seconds to load)
209
230
  When we initially reordered CSS classes in all our templates (~900 Slim files) with the script changing nearly 4000
210
231
  lines, the whole process took less than 30 seconds. This makes the processing speed of approximately 30 files per
211
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
@@ -15,13 +15,16 @@ while (arg = ARGV.shift)
15
15
  when "-c"
16
16
  config_file = ARGV.shift
17
17
  else
18
- file_name = arg
18
+ ARGV.unshift(arg)
19
+ break
19
20
  end
20
21
  end
21
22
 
22
23
  begin
23
- TailwindSorter::Sorter.run(file_name, warn_only: warn_only, config_file: config_file)
24
+ while (file_name = ARGV.shift)
25
+ TailwindSorter.sort_file(file_name, warn_only:, config_file:)
26
+ end
24
27
  rescue ArgumentError => e
25
- STDERR.puts e.message
28
+ warn e.message
26
29
  exit 1
27
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.0"
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.0
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